From 8a5cbb03cf82a5269d2c2ccfe6a856112502d6dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20M=C3=B8nster?= Date: Thu, 11 Aug 2022 12:07:53 +0200 Subject: [PATCH 1/4] Draft for syncing metafields with usync --- .gitignore | 6 +- .../SeoToolkit.Umbraco.Site.csproj | 1 + src/SeoToolkit.Umbraco.sln | 9 + .../Composers/USyncComposer.cs | 14 ++ .../Constants/Serialization.cs | 9 + .../Handlers/MetaFieldValuesHandler.cs | 77 +++++++ .../SeoToolkit.Umbraco.uSync.Core.csproj | 17 ++ .../Serializers/MetaFieldValuesSerializer.cs | 195 ++++++++++++++++++ 8 files changed, 327 insertions(+), 1 deletion(-) create mode 100644 src/SeoToolkit.Umbraco.uSync.Core/Composers/USyncComposer.cs create mode 100644 src/SeoToolkit.Umbraco.uSync.Core/Constants/Serialization.cs create mode 100644 src/SeoToolkit.Umbraco.uSync.Core/Handlers/MetaFieldValuesHandler.cs create mode 100644 src/SeoToolkit.Umbraco.uSync.Core/SeoToolkit.Umbraco.uSync.Core.csproj create mode 100644 src/SeoToolkit.Umbraco.uSync.Core/Serializers/MetaFieldValuesSerializer.cs diff --git a/.gitignore b/.gitignore index f7be36c7..d3f4d447 100644 --- a/.gitignore +++ b/.gitignore @@ -486,4 +486,8 @@ $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/ +**/uSync/ +**/umbraco/Data/Umbraco.sqlite.db +**/umbraco/Data/Umbraco.sqlite.db-shm +**/umbraco/Data/Umbraco.sqlite.db-wal diff --git a/src/SeoToolkit.Umbraco.Site/SeoToolkit.Umbraco.Site.csproj b/src/SeoToolkit.Umbraco.Site/SeoToolkit.Umbraco.Site.csproj index ad95bbf1..f6813ef1 100644 --- a/src/SeoToolkit.Umbraco.Site/SeoToolkit.Umbraco.Site.csproj +++ b/src/SeoToolkit.Umbraco.Site/SeoToolkit.Umbraco.Site.csproj @@ -45,6 +45,7 @@ + diff --git a/src/SeoToolkit.Umbraco.sln b/src/SeoToolkit.Umbraco.sln index 86d06fb2..68fbd0fb 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", "{0080FA37-4D4E-44E0-8235-740C3D557D77}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SeoToolkit.Umbraco.uSync.Core", "SeoToolkit.Umbraco.uSync.Core\SeoToolkit.Umbraco.uSync.Core.csproj", "{A509609B-306B-41ED-9115-C3319ED779D9}" +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 + {A509609B-306B-41ED-9115-C3319ED779D9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A509609B-306B-41ED-9115-C3319ED779D9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A509609B-306B-41ED-9115-C3319ED779D9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A509609B-306B-41ED-9115-C3319ED779D9}.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} + {A509609B-306B-41ED-9115-C3319ED779D9} = {0080FA37-4D4E-44E0-8235-740C3D557D77} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {551ADBA5-3DFB-4300-929C-2BE0DB346636} diff --git a/src/SeoToolkit.Umbraco.uSync.Core/Composers/USyncComposer.cs b/src/SeoToolkit.Umbraco.uSync.Core/Composers/USyncComposer.cs new file mode 100644 index 00000000..692643bc --- /dev/null +++ b/src/SeoToolkit.Umbraco.uSync.Core/Composers/USyncComposer.cs @@ -0,0 +1,14 @@ +using Microsoft.Extensions.DependencyInjection; +using SeoToolkit.Umbraco.uSync.Core.Serializers; +using Umbraco.Cms.Core.Composing; +using Umbraco.Cms.Core.DependencyInjection; + +namespace SeoToolkit.Umbraco.uSync.Core.Composers; + +public class USyncComposer : IComposer +{ + public void Compose(IUmbracoBuilder builder) + { + builder.Services.AddTransient(); + } +} \ No newline at end of file diff --git a/src/SeoToolkit.Umbraco.uSync.Core/Constants/Serialization.cs b/src/SeoToolkit.Umbraco.uSync.Core/Constants/Serialization.cs new file mode 100644 index 00000000..4b584702 --- /dev/null +++ b/src/SeoToolkit.Umbraco.uSync.Core/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.Umbraco.uSync.Core/Handlers/MetaFieldValuesHandler.cs b/src/SeoToolkit.Umbraco.uSync.Core/Handlers/MetaFieldValuesHandler.cs new file mode 100644 index 00000000..ad51db72 --- /dev/null +++ b/src/SeoToolkit.Umbraco.uSync.Core/Handlers/MetaFieldValuesHandler.cs @@ -0,0 +1,77 @@ +using System.Xml.Linq; +using Microsoft.Extensions.Logging; +using SeoToolkit.Umbraco.uSync.Core.Serializers; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Cache; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Strings; +using Umbraco.Extensions; +using uSync.BackOffice.Configuration; +using uSync.BackOffice.Services; +using uSync.Core; +using uSync.Core.Serialization; + +namespace uSync.BackOffice.SyncHandlers.Handlers; + +[SyncHandler("seoToolkitMetaFieldValuesHandler", "SeoToolkit Meta Fields", "seoToolkitContent", uSyncConstants.Priorites.Content + , Icon = "icon-list", IsTwoPass = true, EntityType = Constants.UdiEntityType.Document)] +public class MetaFieldValuesHandler : ContentHandlerBase, ISyncHandler +{ + private readonly MetaFieldValuesSerializer _metaFieldValuesSerializer; + private readonly IContentService _contentService; + + /// + /// the default group for which events matter (content group) + /// + public override string Group => uSyncConstants.Groups.Content; + + public MetaFieldValuesHandler(MetaFieldValuesSerializer metaFieldValuesSerializer,IContentService contentService, ILogger logger, + IEntityService entityService, AppCaches appCaches, IShortStringHelper shortStringHelper, + SyncFileService syncFileService, uSyncEventService mutexService, uSyncConfigService uSyncConfigService, + ISyncItemFactory syncItemFactory) : base(logger, entityService, appCaches, shortStringHelper, syncFileService, + mutexService, uSyncConfigService, syncItemFactory) + { + _metaFieldValuesSerializer = metaFieldValuesSerializer; + _contentService = contentService; + + } + + public override IEnumerable Export(IContent? item, string folder, HandlerSettings config) + { + if (item == null) + return uSyncAction.Fail(nameof(item), typeof(IContent).ToString(), ChangeType.Fail, "Item not set", + new ArgumentNullException(nameof(item))).AsEnumerableOfOne(); + + var filename = GetPath(folder, item, config.GuidNames, config.UseFlatStructure) + .ToAppSafeFileName(); + + var attempt = _metaFieldValuesSerializer.Serialize(item, new SyncSerializerOptions(config.Settings)); + if (!attempt.Success) + return uSyncActionHelper.SetAction(attempt, filename, GetItemKey(item), this.Alias) + .AsEnumerableOfOne(); + + if (ShouldExport(attempt.Item, config)) + { + // only write the file to disk if it should be exported. + syncFileService.SaveXElement(attempt.Item, filename); + } + else + { + return uSyncAction.SetAction(true, filename, type: typeof(IContent).ToString(), + change: ChangeType.NoChange, message: "Not Exported (Based on config)", filename: filename) + .AsEnumerableOfOne(); + } + + return uSyncActionHelper.SetAction(attempt, filename, GetItemKey(item), this.Alias) + .AsEnumerableOfOne(); + } + + + public override IEnumerable Import(XElement node, string filename, HandlerSettings config, SerializerFlags flags) + { + var attempt = _metaFieldValuesSerializer.Deserialize(node, new SyncSerializerOptions(config.Settings)); + return uSyncActionHelper.SetAction(attempt, filename,node.GetKey(), this.Alias) + .AsEnumerableOfOne(); + } +} \ No newline at end of file diff --git a/src/SeoToolkit.Umbraco.uSync.Core/SeoToolkit.Umbraco.uSync.Core.csproj b/src/SeoToolkit.Umbraco.uSync.Core/SeoToolkit.Umbraco.uSync.Core.csproj new file mode 100644 index 00000000..892171d1 --- /dev/null +++ b/src/SeoToolkit.Umbraco.uSync.Core/SeoToolkit.Umbraco.uSync.Core.csproj @@ -0,0 +1,17 @@ + + + + net6.0 + enable + enable + + + + + + + + + + + diff --git a/src/SeoToolkit.Umbraco.uSync.Core/Serializers/MetaFieldValuesSerializer.cs b/src/SeoToolkit.Umbraco.uSync.Core/Serializers/MetaFieldValuesSerializer.cs new file mode 100644 index 00000000..21a72f27 --- /dev/null +++ b/src/SeoToolkit.Umbraco.uSync.Core/Serializers/MetaFieldValuesSerializer.cs @@ -0,0 +1,195 @@ +using System.Drawing; +using System.Xml.Linq; +using Microsoft.Extensions.Logging; +using SeoToolkit.Umbraco.MetaFields.Core.Interfaces.Services; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.Entities; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Strings; +using Umbraco.Extensions; +using uSync.Core; +using uSync.Core.Mapping; +using uSync.Core.Models; +using uSync.Core.Serialization; +using uSync.Core.Serialization.Serializers; + +namespace SeoToolkit.Umbraco.uSync.Core.Serializers; + +[SyncSerializer("cfeaa2d0-8af5-4ef4-9dd6-3b696df18189", "SeoToolkit.MetaFieldSerializer", Constants.Serialization.MetaFieldValues)] +public class MetaFieldValuesSerializer : ContentSerializerBase +{ + private readonly IShortStringHelper _shortStringHelper; + private readonly IEntityService _entityService; + private readonly IMetaFieldsValueService _metaFieldsValueService; + private readonly IContentService _contentService; + + public MetaFieldValuesSerializer(IEntityService entityService, ILocalizationService localizationService, + IRelationService relationService, IShortStringHelper shortStringHelper, + ILogger logger, + SyncValueMapperCollection syncMappers, IMetaFieldsValueService metaFieldsValueService, + IContentService contentService) : base(entityService, localizationService, relationService, shortStringHelper, + logger, UmbracoObjectTypes.Document, syncMappers) + { + _entityService = entityService; + _shortStringHelper = shortStringHelper; + _metaFieldsValueService = metaFieldsValueService; + _contentService = contentService; + } + + private XElement InitializeNode(IContent item) + { + return new XElement(Constants.Serialization.RootName, new XAttribute("Key", ItemKey(item)), new XAttribute("Alias", ItemAlias(item))); + } + + public new SyncAttempt Deserialize(XElement node, SyncSerializerOptions options) + { + if (node.IsEmptyItem()) + return ProcessAction(node, options); + if (!IsValid(node)) + throw new FormatException("XML Not valid for type " + this.ItemType); + if (!options.Force && this.IsCurrent(node, options) <= ChangeType.NoChange) + return SyncAttempt.Succeed(node.GetAlias(), ChangeType.NoChange); + var syncAttempt1 = this.CanDeserialize(node, options); + if (!syncAttempt1.Success) + return syncAttempt1; + logger.LogDebug("Base: Deserializing {0}", (object) this.ItemType); + var syncAttempt2 = DeserializeCore(node, options); + if (syncAttempt2.Success) + { + logger.LogDebug("Base: Deserialize Core Success {0}", (object) this.ItemType); + if (!syncAttempt2.Saved && !options.Flags.HasFlag((Enum) SerializerFlags.DoNotSave)) + { + logger.LogDebug("Base: Serializer Saving (No DoNotSaveFlag) {0}", (object) this.ItemAlias(syncAttempt2.Item)); + SaveItem(syncAttempt2.Item); + } + if (options.OnePass) + { + logger.LogDebug("Base: Processing item in one pass {0}", (object) this.ItemAlias(syncAttempt2.Item)); + var syncAttempt3 = this.DeserializeSecondPass(syncAttempt2.Item, node, options); + logger.LogDebug("Base: Second Pass Result {0} {1}", (object) this.ItemAlias(syncAttempt2.Item), (object) syncAttempt3.Success); + return syncAttempt3; + } + } + return syncAttempt2; + } + + protected override SyncAttempt SerializeCore(IContent item, SyncSerializerOptions options) + { + var node = InitializeNode(item); + + var fields = _metaFieldsValueService.GetUserValues(item.Id); + + if (!fields.Any()) + { + return SyncAttempt.Fail(item.Name, ChangeType.NoChange, "No Meta Fields"); + } + + node.Add(fields.Select(field => new XElement(field.Key, field.Value))); + + return SyncAttempt.Succeed(item.Name, node, typeof(IContent), ChangeType.Export); + } + + protected override SyncAttempt DeserializeCore(XElement node, SyncSerializerOptions options) + { + var attempt = FindOrCreate(node); + var item = GetDictionaryValues(node); + if (attempt.Success && attempt.Result != null) + { + Save(attempt.Result.Id, item); + return SyncAttempt.Succeed(node.GetAlias(), attempt.Result, + typeof(IContent), ChangeType.Import); + } + + + return SyncAttempt.Fail(node.GetAlias(), attempt.Result, ChangeType.Fail, + "Failed to find or create content", attempt.Exception); + } + + public override bool IsValid(XElement node) => node != null! && node.GetAlias() != null && node.Name == Constants.Serialization.RootName; + + public override ChangeType IsCurrent(XElement node, SyncSerializerOptions options) + { + var content = _contentService.GetById(node.GetKey()); + var currentValues = _metaFieldsValueService.GetUserValues(content.Id); + var newValues = GetDictionaryValues(node); + + if (currentValues.Count != newValues.Count) + return ChangeType.Import; + + if (currentValues.Intersect(newValues).Count() != currentValues.Count) + return ChangeType.Import; + + + return ChangeType.NoChange; + } + + public override IContent FindItem(int id) + { + var item = _contentService.GetById(id); + if (item != null) + { + AddToNameCache(id, item.Key, item.Name); + return item; + } + + return null!; + } + + + public override IContent FindItem(Guid key) + => _contentService.GetById(key)!; + + public override void SaveItem(IContent item) + { + //We do not need to save the contentItem itself, but we do need to override this method + } + + public override void DeleteItem(IContent item) + { + //We do not need to delete the contentItem itself, but we do need to override this method + //Maybe we should delete meta-field values for a content item when it is deleted? + } + + + private void Save(int nodeId, Dictionary item) + { + _metaFieldsValueService.AddValues(nodeId, item); + } + + private Dictionary GetDictionaryValues(XElement node) + { + return node.Descendants() + .ToDictionary(descendantNode => descendantNode.Name.ToString(), + descendantNode => descendantNode.Value); + } + + + protected override Attempt CreateItem(string alias, ITreeEntity parent, string itemType) + { + var parentId = parent != null! ? parent.Id : -1; + var item = _contentService.Create(alias, parentId, itemType); + if (item == null!) + return Attempt.Fail(item, new ArgumentException($"Unable to create content item of type {itemType}"))!; + + return Attempt.Succeed(item)!; + } + + protected override uSyncChange HandleTrashedState(IContent item, bool trashed) + { + // We do not need to handle trashed state, the default handler does this for us. + return null!; + + } + + protected override IContent FindAtRoot(string alias) + { + var rootNodes = _contentService.GetRootContent().ToList(); + if (rootNodes.Any()) + { + return rootNodes.FirstOrDefault(x => x.Name!.ToSafeAlias(shortStringHelper).InvariantEquals(alias))!; + } + + return null!; + } +} \ No newline at end of file From 0047634ed4de5ad135d93f2bd670c1ea80f4754f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20M=C3=B8nster?= Date: Thu, 11 Aug 2022 12:18:54 +0200 Subject: [PATCH 2/4] Update folder name --- .../Handlers/MetaFieldValuesHandler.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/SeoToolkit.Umbraco.uSync.Core/Handlers/MetaFieldValuesHandler.cs b/src/SeoToolkit.Umbraco.uSync.Core/Handlers/MetaFieldValuesHandler.cs index ad51db72..e0de6fc0 100644 --- a/src/SeoToolkit.Umbraco.uSync.Core/Handlers/MetaFieldValuesHandler.cs +++ b/src/SeoToolkit.Umbraco.uSync.Core/Handlers/MetaFieldValuesHandler.cs @@ -14,7 +14,7 @@ namespace uSync.BackOffice.SyncHandlers.Handlers; -[SyncHandler("seoToolkitMetaFieldValuesHandler", "SeoToolkit Meta Fields", "seoToolkitContent", uSyncConstants.Priorites.Content +[SyncHandler("seoToolkitMetaFieldValuesHandler", "SeoToolkit Meta Fields", "SeoToolkitMetaFields", uSyncConstants.Priorites.Content , Icon = "icon-list", IsTwoPass = true, EntityType = Constants.UdiEntityType.Document)] public class MetaFieldValuesHandler : ContentHandlerBase, ISyncHandler { From 5df6d6ddcda384c5d61832af24404149dcf221c0 Mon Sep 17 00:00:00 2001 From: Patrick de Mooij Date: Thu, 11 Aug 2022 21:14:00 +0200 Subject: [PATCH 3/4] Fix uSync error on startup --- .../Services/MetaFieldsValueService/MetaFieldsValueService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/SeoToolkit.Umbraco.MetaFields.Core/Services/MetaFieldsValueService/MetaFieldsValueService.cs b/src/SeoToolkit.Umbraco.MetaFields.Core/Services/MetaFieldsValueService/MetaFieldsValueService.cs index 5e33e0a4..e69220fc 100644 --- a/src/SeoToolkit.Umbraco.MetaFields.Core/Services/MetaFieldsValueService/MetaFieldsValueService.cs +++ b/src/SeoToolkit.Umbraco.MetaFields.Core/Services/MetaFieldsValueService/MetaFieldsValueService.cs @@ -50,7 +50,7 @@ public void Delete(int nodeId, string fieldAlias) private string GetCulture() { - return _variationContextAccessor.VariationContext.Culture; + return _variationContextAccessor.VariationContext?.Culture ?? string.Empty; } private void ClearCache(int nodeId) From 0e48ee339a0cf479ed88be6e2d49aafd73c222da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20M=C3=B8nster?= Date: Fri, 12 Aug 2022 12:50:44 +0200 Subject: [PATCH 4/4] Adds reporting and adds handling for language variants --- .../Services/IMetaFieldsValueService.cs | 4 +- .../MetaFieldsValueService.cs | 17 ++- .../Composers/USyncComposer.cs | 4 + .../Handlers/MetaFieldValuesHandler.cs | 39 +++++- .../Serializers/MetaFieldValuesSerializer.cs | 131 +++++++++++------- .../XmlTrackers/SeoToolkitXmlTracker.cs | 29 ++++ 6 files changed, 162 insertions(+), 62 deletions(-) create mode 100644 src/SeoToolkit.Umbraco.uSync.Core/XmlTrackers/SeoToolkitXmlTracker.cs diff --git a/src/SeoToolkit.Umbraco.MetaFields.Core/Interfaces/Services/IMetaFieldsValueService.cs b/src/SeoToolkit.Umbraco.MetaFields.Core/Interfaces/Services/IMetaFieldsValueService.cs index 43450e8e..3a1ec0f7 100644 --- a/src/SeoToolkit.Umbraco.MetaFields.Core/Interfaces/Services/IMetaFieldsValueService.cs +++ b/src/SeoToolkit.Umbraco.MetaFields.Core/Interfaces/Services/IMetaFieldsValueService.cs @@ -6,8 +6,8 @@ namespace SeoToolkit.Umbraco.MetaFields.Core.Interfaces.Services { public interface IMetaFieldsValueService { - Dictionary GetUserValues(int nodeId); - void AddValues(int nodeId, Dictionary values); + Dictionary GetUserValues(int nodeId, string culture = null); + void AddValues(int nodeId, Dictionary values, string culture = null); void Delete(int nodeId, string fieldAlias); } } diff --git a/src/SeoToolkit.Umbraco.MetaFields.Core/Services/MetaFieldsValueService/MetaFieldsValueService.cs b/src/SeoToolkit.Umbraco.MetaFields.Core/Services/MetaFieldsValueService/MetaFieldsValueService.cs index e69220fc..c4199cb7 100644 --- a/src/SeoToolkit.Umbraco.MetaFields.Core/Services/MetaFieldsValueService/MetaFieldsValueService.cs +++ b/src/SeoToolkit.Umbraco.MetaFields.Core/Services/MetaFieldsValueService/MetaFieldsValueService.cs @@ -23,26 +23,29 @@ public MetaFieldsValueService(IMetaFieldsValueRepository repository, IVariationC _cache = appCaches.RuntimeCache; } - public Dictionary GetUserValues(int nodeId) + public Dictionary GetUserValues(int nodeId, string culture = null) { - var culture = GetCulture(); - return _cache.GetCacheItem($"{BaseCacheKey}{nodeId}_{culture}", () => _repository.GetAllValues(nodeId, culture), TimeSpan.FromMinutes(30)); + var iso = culture ?? GetCulture(); + return _cache.GetCacheItem($"{BaseCacheKey}{nodeId}_{iso}", () => _repository.GetAllValues(nodeId, iso), TimeSpan.FromMinutes(30)); } - public void AddValues(int nodeId, Dictionary values) + public void AddValues(int nodeId, Dictionary values, string culture = null) { + var iso = culture ?? GetCulture(); foreach (var (key, value) in values) { - if (_repository.Exists(nodeId, key, GetCulture())) - _repository.Update(nodeId, key, GetCulture(), value); + if (_repository.Exists(nodeId, key, iso)) + _repository.Update(nodeId, key, iso, value); else { - _repository.Add(nodeId, key, GetCulture(), value); + _repository.Add(nodeId, key, iso, value); } } ClearCache(nodeId); } + + public void Delete(int nodeId, string fieldAlias) { _repository.Delete(nodeId, fieldAlias, GetCulture()); diff --git a/src/SeoToolkit.Umbraco.uSync.Core/Composers/USyncComposer.cs b/src/SeoToolkit.Umbraco.uSync.Core/Composers/USyncComposer.cs index 692643bc..ebd6512b 100644 --- a/src/SeoToolkit.Umbraco.uSync.Core/Composers/USyncComposer.cs +++ b/src/SeoToolkit.Umbraco.uSync.Core/Composers/USyncComposer.cs @@ -1,7 +1,10 @@ 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 Umbraco.Cms.Core.Models; +using uSync.Core.Tracking; namespace SeoToolkit.Umbraco.uSync.Core.Composers; @@ -10,5 +13,6 @@ public class USyncComposer : IComposer public void Compose(IUmbracoBuilder builder) { builder.Services.AddTransient(); + builder.Services.AddTransient(); } } \ No newline at end of file diff --git a/src/SeoToolkit.Umbraco.uSync.Core/Handlers/MetaFieldValuesHandler.cs b/src/SeoToolkit.Umbraco.uSync.Core/Handlers/MetaFieldValuesHandler.cs index e0de6fc0..b8e0616b 100644 --- a/src/SeoToolkit.Umbraco.uSync.Core/Handlers/MetaFieldValuesHandler.cs +++ b/src/SeoToolkit.Umbraco.uSync.Core/Handlers/MetaFieldValuesHandler.cs @@ -1,6 +1,8 @@ using System.Xml.Linq; using Microsoft.Extensions.Logging; +using SeoToolkit.Umbraco.MetaFields.Core.Interfaces.Services; using SeoToolkit.Umbraco.uSync.Core.Serializers; +using SeoToolkit.Umbraco.uSync.Core.XmlTrackers; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Models; @@ -18,6 +20,7 @@ namespace uSync.BackOffice.SyncHandlers.Handlers; , Icon = "icon-list", IsTwoPass = true, EntityType = Constants.UdiEntityType.Document)] public class MetaFieldValuesHandler : ContentHandlerBase, ISyncHandler { + private readonly IMetaFieldsValueService _metaFieldsValueService; private readonly MetaFieldValuesSerializer _metaFieldValuesSerializer; private readonly IContentService _contentService; @@ -32,9 +35,11 @@ public MetaFieldValuesHandler(MetaFieldValuesSerializer metaFieldValuesSerialize ISyncItemFactory syncItemFactory) : base(logger, entityService, appCaches, shortStringHelper, syncFileService, mutexService, uSyncConfigService, syncItemFactory) { + _metaFieldValuesSerializer = metaFieldValuesSerializer; _contentService = contentService; - + + this.serializer = _metaFieldValuesSerializer as ISyncSerializer; } public override IEnumerable Export(IContent? item, string folder, HandlerSettings config) @@ -70,8 +75,36 @@ public override IEnumerable Export(IContent? item, string folder, H public override IEnumerable Import(XElement node, string filename, HandlerSettings config, SerializerFlags flags) { - var attempt = _metaFieldValuesSerializer.Deserialize(node, new SyncSerializerOptions(config.Settings)); - return uSyncActionHelper.SetAction(attempt, filename,node.GetKey(), this.Alias) + if (ShouldImport(node, new HandlerSettings())) + { + var attempt = _metaFieldValuesSerializer.Deserialize(node, new SyncSerializerOptions(config.Settings)); + return uSyncActionHelper.SetAction(attempt, filename,node.GetKey(), this.Alias) + .AsEnumerableOfOne(); + } + + return uSyncAction.SetAction(true, filename, type: typeof(IContent).ToString(), + change: ChangeType.NoChange, message: "Not Imported (Based on config)", filename: filename) .AsEnumerableOfOne(); } + + + public override IEnumerable ReportFolder(string folder, HandlerSettings config, SyncUpdateCallback callback) + { + var reportActions= base.ReportFolder(folder, config, callback).ToList(); + reportActions = reportActions.Select(action => + { + if (!action.Details.Any()) + { + return action; + } + + var details = action.Details.ToList(); + //First item will always be an error since the default XmlTracker will have a xml schema mismatch error. + details.RemoveAt(0); + action.Details = details; + return action; + }).ToList(); + + return reportActions; + } } \ No newline at end of file diff --git a/src/SeoToolkit.Umbraco.uSync.Core/Serializers/MetaFieldValuesSerializer.cs b/src/SeoToolkit.Umbraco.uSync.Core/Serializers/MetaFieldValuesSerializer.cs index 21a72f27..f948ab80 100644 --- a/src/SeoToolkit.Umbraco.uSync.Core/Serializers/MetaFieldValuesSerializer.cs +++ b/src/SeoToolkit.Umbraco.uSync.Core/Serializers/MetaFieldValuesSerializer.cs @@ -2,6 +2,7 @@ using System.Xml.Linq; using Microsoft.Extensions.Logging; using SeoToolkit.Umbraco.MetaFields.Core.Interfaces.Services; +using SeoToolkit.Umbraco.MetaFields.Core.Repositories.SeoValueRepository; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Entities; @@ -16,11 +17,11 @@ namespace SeoToolkit.Umbraco.uSync.Core.Serializers; -[SyncSerializer("cfeaa2d0-8af5-4ef4-9dd6-3b696df18189", "SeoToolkit.MetaFieldSerializer", Constants.Serialization.MetaFieldValues)] -public class MetaFieldValuesSerializer : ContentSerializerBase +[SyncSerializer("cfeaa2d0-8af5-4ef4-9dd6-3b696df18189", "SeoToolkit.MetaFieldSerializer", + Constants.Serialization.MetaFieldValues)] +public class MetaFieldValuesSerializer : ContentSerializerBase, ISyncSerializer { - private readonly IShortStringHelper _shortStringHelper; - private readonly IEntityService _entityService; + private readonly ILocalizationService _localizationService; private readonly IMetaFieldsValueService _metaFieldsValueService; private readonly IContentService _contentService; @@ -31,15 +32,15 @@ public MetaFieldValuesSerializer(IEntityService entityService, ILocalizationServ IContentService contentService) : base(entityService, localizationService, relationService, shortStringHelper, logger, UmbracoObjectTypes.Document, syncMappers) { - _entityService = entityService; - _shortStringHelper = shortStringHelper; + _localizationService = localizationService; _metaFieldsValueService = metaFieldsValueService; _contentService = contentService; } private XElement InitializeNode(IContent item) { - return new XElement(Constants.Serialization.RootName, new XAttribute("Key", ItemKey(item)), new XAttribute("Alias", ItemAlias(item))); + return new XElement(Constants.Serialization.RootName, new XAttribute("Key", ItemKey(item)), + new XAttribute("Alias", ItemAlias(item))); } public new SyncAttempt Deserialize(XElement node, SyncSerializerOptions options) @@ -53,77 +54,103 @@ private XElement InitializeNode(IContent item) var syncAttempt1 = this.CanDeserialize(node, options); if (!syncAttempt1.Success) return syncAttempt1; - logger.LogDebug("Base: Deserializing {0}", (object) this.ItemType); + logger.LogDebug($"Base: Deserializing {0}", (object)this.ItemType); var syncAttempt2 = DeserializeCore(node, options); if (syncAttempt2.Success) { - logger.LogDebug("Base: Deserialize Core Success {0}", (object) this.ItemType); - if (!syncAttempt2.Saved && !options.Flags.HasFlag((Enum) SerializerFlags.DoNotSave)) + logger.LogDebug("Base: Deserialize Core Success {0}", (object)this.ItemType); + if (!syncAttempt2.Saved && !options.Flags.HasFlag((Enum)SerializerFlags.DoNotSave)) { - logger.LogDebug("Base: Serializer Saving (No DoNotSaveFlag) {0}", (object) this.ItemAlias(syncAttempt2.Item)); + logger.LogDebug("Base: Serializer Saving (No DoNotSaveFlag) {0}", + (object)this.ItemAlias(syncAttempt2.Item)); SaveItem(syncAttempt2.Item); } + if (options.OnePass) { - logger.LogDebug("Base: Processing item in one pass {0}", (object) this.ItemAlias(syncAttempt2.Item)); + logger.LogDebug("Base: Processing item in one pass {0}", (object)this.ItemAlias(syncAttempt2.Item)); var syncAttempt3 = this.DeserializeSecondPass(syncAttempt2.Item, node, options); - logger.LogDebug("Base: Second Pass Result {0} {1}", (object) this.ItemAlias(syncAttempt2.Item), (object) syncAttempt3.Success); + logger.LogDebug("Base: Second Pass Result {0} {1}", (object)this.ItemAlias(syncAttempt2.Item), + (object)syncAttempt3.Success); return syncAttempt3; } } + return syncAttempt2; } + private const string Value = "Value"; + private const string Culture = "Culture"; + protected override SyncAttempt SerializeCore(IContent item, SyncSerializerOptions options) { var node = InitializeNode(item); - var fields = _metaFieldsValueService.GetUserValues(item.Id); + var languageVariants = _localizationService.GetAllLanguages(); - if (!fields.Any()) + foreach (var languageVariant in languageVariants) { - return SyncAttempt.Fail(item.Name, ChangeType.NoChange, "No Meta Fields"); + var fields = _metaFieldsValueService.GetUserValues(item.Id, languageVariant.IsoCode); + + if (!fields.Any()) + { + return SyncAttempt.Fail(item.Name, ChangeType.NoChange, "No Meta Fields"); + } + + foreach (var field in fields) + { + var el = node.Element(field.Key); + if (el != null) + { + el.Elements(Value).FirstOrDefault(e => + e.HasAttributes && e.Attribute(Culture)?.Value == languageVariant.IsoCode)?.Remove(); + + + el.Add(new XElement(Value, field.Value, new XAttribute(Culture, languageVariant.IsoCode))); + } + else + { + node.Add(fields.Select(field => + { + var element = new XElement(field.Key, + new XElement(Value, field.Value, new XAttribute(Culture, languageVariant.IsoCode))); + return element; + })); + } + } } - node.Add(fields.Select(field => new XElement(field.Key, field.Value))); return SyncAttempt.Succeed(item.Name, node, typeof(IContent), ChangeType.Export); } protected override SyncAttempt DeserializeCore(XElement node, SyncSerializerOptions options) { + var languageVariants = _localizationService.GetAllLanguages(); + var attempt = FindOrCreate(node); - var item = GetDictionaryValues(node); - if (attempt.Success && attempt.Result != null) + + if (!attempt.Success || attempt.Result == null) + return SyncAttempt.Fail(node.GetAlias(), attempt.Result!, ChangeType.Fail, + "Failed to find or create content", attempt.Exception); + + foreach (var languageVariant in languageVariants) { - Save(attempt.Result.Id, item); - return SyncAttempt.Succeed(node.GetAlias(), attempt.Result, - typeof(IContent), ChangeType.Import); - } + var item = GetDictionaryValues(node, languageVariant.IsoCode); + Save(attempt.Result.Id, item!, languageVariant.IsoCode); + } - return SyncAttempt.Fail(node.GetAlias(), attempt.Result, ChangeType.Fail, - "Failed to find or create content", attempt.Exception); + return SyncAttempt.Succeed(node.GetAlias(), attempt.Result, + typeof(IContent), ChangeType.Import); } - public override bool IsValid(XElement node) => node != null! && node.GetAlias() != null && node.Name == Constants.Serialization.RootName; - - public override ChangeType IsCurrent(XElement node, SyncSerializerOptions options) + public override bool IsValid(XElement node) { - var content = _contentService.GetById(node.GetKey()); - var currentValues = _metaFieldsValueService.GetUserValues(content.Id); - var newValues = GetDictionaryValues(node); - - if (currentValues.Count != newValues.Count) - return ChangeType.Import; - - if (currentValues.Intersect(newValues).Count() != currentValues.Count) - return ChangeType.Import; - - - return ChangeType.NoChange; + return node != null! && node.GetAlias() != null && node.Name == Constants.Serialization.RootName; } - + + public override IContent FindItem(int id) { var item = _contentService.GetById(id); @@ -135,14 +162,14 @@ public override IContent FindItem(int id) return null!; } - + public override IContent FindItem(Guid key) => _contentService.GetById(key)!; public override void SaveItem(IContent item) { - //We do not need to save the contentItem itself, but we do need to override this method + //We do not need to save the contentItem itself, but we do need to override this method } public override void DeleteItem(IContent item) @@ -152,16 +179,21 @@ public override void DeleteItem(IContent item) } - private void Save(int nodeId, Dictionary item) + private void Save(int nodeId, Dictionary item, string culture) { - _metaFieldsValueService.AddValues(nodeId, item); + _metaFieldsValueService.AddValues(nodeId, item, culture); } - private Dictionary GetDictionaryValues(XElement node) + public Dictionary GetDictionaryValues(XElement node, string culture) { - return node.Descendants() - .ToDictionary(descendantNode => descendantNode.Name.ToString(), - descendantNode => descendantNode.Value); + return node.Elements() + .ToDictionary( + descendantNode => descendantNode.Name.ToString(), + descendantNode => + { + return descendantNode.Elements(Value) + .FirstOrDefault(e => e.HasAttributes && e.Attribute(Culture)?.Value == culture)?.Value!; + }); } @@ -179,7 +211,6 @@ protected override uSyncChange HandleTrashedState(IContent item, bool trashed) { // We do not need to handle trashed state, the default handler does this for us. return null!; - } protected override IContent FindAtRoot(string alias) diff --git a/src/SeoToolkit.Umbraco.uSync.Core/XmlTrackers/SeoToolkitXmlTracker.cs b/src/SeoToolkit.Umbraco.uSync.Core/XmlTrackers/SeoToolkitXmlTracker.cs new file mode 100644 index 00000000..781aed89 --- /dev/null +++ b/src/SeoToolkit.Umbraco.uSync.Core/XmlTrackers/SeoToolkitXmlTracker.cs @@ -0,0 +1,29 @@ +using System.Xml.Linq; +using SeoToolkit.Umbraco.uSync.Core.Serializers; +using Umbraco.Cms.Core.Models; +using uSync.Core; +using uSync.Core.Models; +using uSync.Core.Serialization; +using uSync.Core.Tracking; +using uSync.Core.Tracking.Impliment; + +namespace SeoToolkit.Umbraco.uSync.Core.XmlTrackers; + +public class SeoToolkitXmlTracker : + SyncXmlTracker, + ISyncTracker, + ISyncTrackerBase +{ + 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