diff --git a/uSync.Core/Extensions/JsonTextExtensions.cs b/uSync.Core/Extensions/JsonTextExtensions.cs
index 8f8a04129..b67e1d01c 100644
--- a/uSync.Core/Extensions/JsonTextExtensions.cs
+++ b/uSync.Core/Extensions/JsonTextExtensions.cs
@@ -31,6 +31,8 @@ public static class JsonTextExtensions
new JsonUdiRangeConverter(),
new JsonBooleanConverter(),
new JsonXElementConverter(),
+ new JsonBlockListLayoutItemConverter(),
+ new JsonBlockGridLayoutItemConverter()
}
};
diff --git a/uSync.Core/Json/JsonBlockItemConverters.cs b/uSync.Core/Json/JsonBlockItemConverters.cs
new file mode 100644
index 000000000..529b65d87
--- /dev/null
+++ b/uSync.Core/Json/JsonBlockItemConverters.cs
@@ -0,0 +1,78 @@
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+using Umbraco.Cms.Core.Models.Blocks;
+
+namespace uSync.Core.Json;
+
+///
+/// in v16 the BlockLayoutItem(s) have obsolete properties that are sometimes set and sometimes not
+/// this leads to false positive's when looking for changes.
+///
+/// these two custom converters write those properties out as null, so they never change. causing
+/// the serialized json to be consistent.
+///
+
+public abstract class JsonBlockItemConverterBase : JsonConverter
+ where T : BlockLayoutItemBase, new()
+{
+ public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ if (reader.TokenType != JsonTokenType.StartObject)
+ throw new JsonException("Invalid JSON expecting start object");
+
+ var item = new T();
+
+ while (reader.Read())
+ {
+ if (reader.TokenType == JsonTokenType.EndObject)
+ return item;
+
+ if (reader.TokenType != JsonTokenType.PropertyName)
+ throw new JsonException("Expecting property name");
+
+ var propertyName = reader.GetString();
+ reader.Read();
+
+ switch (propertyName)
+ {
+ case "contentKey":
+ var contentKey = reader.GetString();
+ if (contentKey != null && Guid.TryParse(contentKey, out var contentGuid))
+ item.ContentKey = contentGuid;
+ break;
+ case "settingsKey":
+ var settingsKey = reader.GetString();
+ if (settingsKey != null && Guid.TryParse(settingsKey, out var settingsGuid))
+ item.SettingsKey = settingsGuid;
+ break;
+ default:
+ // we don't care about the obsolete properties here...
+ break;
+ }
+ }
+
+ throw new JsonException("Unexpected end of JSON");
+ }
+
+ public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
+ {
+ // we could just not write out the obsolete properties, but they will appear as null it
+ // lots of people's existing exports , so we can write them out as null to keep things consistent.
+ writer.WriteStartObject();
+ writer.WriteString("contentKey", value.ContentKey.ToString());
+ writer.WriteNull("contentUdi");
+
+ if (value.SettingsKey.HasValue && value.SettingsKey != Guid.Empty)
+ writer.WriteString("settingsKey", value.SettingsKey.ToString());
+ else
+ writer.WriteNull("settingsKey");
+
+ writer.WriteNull("settingsUdi");
+ writer.WriteEndObject();
+ }
+}
+
+public class JsonBlockListLayoutItemConverter : JsonBlockItemConverterBase { }
+
+public class JsonBlockGridLayoutItemConverter : JsonBlockItemConverterBase { }
diff --git a/uSync.Core/Json/XElementJsonConverter.cs b/uSync.Core/Json/XElementJsonConverter.cs
index 22b2a392b..23959e33e 100644
--- a/uSync.Core/Json/XElementJsonConverter.cs
+++ b/uSync.Core/Json/XElementJsonConverter.cs
@@ -16,3 +16,4 @@ public override void Write(Utf8JsonWriter writer, XElement value, JsonSerializer
writer.WriteStringValue(value.ToString());
}
}
+
diff --git a/uSync.Core/Serialization/Serializers/ContentSerializer.cs b/uSync.Core/Serialization/Serializers/ContentSerializer.cs
index 95cfd0e4d..1f41ecb6f 100644
--- a/uSync.Core/Serialization/Serializers/ContentSerializer.cs
+++ b/uSync.Core/Serialization/Serializers/ContentSerializer.cs
@@ -175,19 +175,6 @@ protected override async Task> DeserializeCoreAsync(XEleme
details.AddRange(await DeserializeBaseAsync(item, node, options));
- var infoNode = node.Element(uSyncConstants.Xml.Info);
-
- if (infoNode is not null)
- {
- var trashed = infoNode.Element("Trashed").ValueOrDefault(false);
- var restoreParent = infoNode.Element("Trashed")?.Attribute("Parent").ValueOrDefault(Guid.Empty) ?? Guid.Empty;
- details.AddNotNull(HandleTrashedState(item, trashed, restoreParent));
- }
-
- // cultures...
-
-
-
details.AddNotNull(await DeserializeTemplate(item, node));
var propertiesAttempt = await DeserializePropertiesAsync(item, node, options);
@@ -303,11 +290,15 @@ public int DeserializeWriterInfo(IContent item, XElement node, SyncSerializerOpt
///
public override async Task> DeserializeSecondPassAsync(IContent item, XElement node, SyncSerializerOptions options)
{
+ var details = new List();
+
+ // move trashed state to second pass, as the item needs an Id for the relation to work.
+ details.AddNotNull(await DeserializeTrashed(node, item, Constants.Conventions.RelationTypes.RelateParentDocumentOnDeleteAlias));
+
// move sort to second pass, as if we attempt to set this
// on a brand new item, it doesn't get set.
// doing it on second pass ensures it gets set on the item
// after it has been saved by umbraco.
- var details = new List();
if (!options.GetSetting(
uSyncConstants.DefaultSettings.IgnoreSortOrder,
uSyncConstants.DefaultSettings.IgnoreSortOrder_Default))
@@ -417,41 +408,12 @@ private static ContentSchedule GetContentScheduleFromNode(XElement scheduleNode)
return null;
}
+ // trashed helpers.
+ protected override void MoveToRecycleBin(IContent item) => contentService.MoveToRecycleBin(item);
+ protected override void SetTrashed(IContent item) => ((ContentBase)item).Trashed = true;
+ protected override void MoveItem(IContent item, int parentId) => contentService.Move(item, parentId);
+ protected override IContent? GetByKey(Guid id) => contentService.GetById(id);
- protected override uSyncChange? HandleTrashedState(IContent item, bool trashed, Guid restoreParentKey)
- {
- if (!trashed && item.Trashed)
- {
- // if the item is trashed, then the change of it's parent
- // should restore it (as long as we do a move!)
-
-
- var restoreParentId = GetRelationParentId(item, restoreParentKey, Constants.Conventions.RelationTypes.RelateParentDocumentOnDeleteAlias);
- contentService.Move(item, restoreParentId);
-
- // clean out any relations for this item (some versions of Umbraco don't do this on a Move)
- CleanRelations(item, Constants.Conventions.RelationTypes.RelateParentDocumentOnDeleteAlias);
-
- return uSyncChange.Update("Restored", item.Name ?? item.Id.ToString(), "Recycle Bin", restoreParentKey.ToString());
-
- }
- else if (trashed && !item.Trashed)
- {
- // not already in the recycle bin?
- if (item.ParentId > Constants.System.RecycleBinContent)
- {
- // clean any relations that may be there (stops an error)
- CleanRelations(item, Constants.Conventions.RelationTypes.RelateParentDocumentOnDeleteAlias);
-
- // move to the recycle bin
- contentService.MoveToRecycleBin(item);
- }
-
- return uSyncChange.Update("Moved to Bin", item.Name ?? item.Id.ToString(), "", "Recycle Bin");
- }
-
- return null;
- }
protected virtual Attempt DoSaveOrPublish(IContent item, XElement node, SyncSerializerOptions options)
{
diff --git a/uSync.Core/Serialization/Serializers/ContentSerializerBase.cs b/uSync.Core/Serialization/Serializers/ContentSerializerBase.cs
index db9bcdcef..fd9ffc3ef 100644
--- a/uSync.Core/Serialization/Serializers/ContentSerializerBase.cs
+++ b/uSync.Core/Serialization/Serializers/ContentSerializerBase.cs
@@ -1,4 +1,4 @@
-using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Logging;
using System.Globalization;
using System.Text.RegularExpressions;
@@ -649,9 +649,69 @@ private static bool IsUpdatedValue(object? current, object? newValue)
return null;
}
+ protected virtual void MoveToRecycleBin(TObject item) { }
+ protected virtual void SetTrashed(TObject item) { }
+ protected virtual void MoveItem(TObject item, int parentId) { }
+ protected virtual TObject? GetByKey(Guid id) => default;
+
+ protected async Task DeserializeTrashed(XElement node, TObject item, string relationAlias)
+ {
+ var info = node.Element(uSyncConstants.Xml.Info);
+ if (info is null) return null;
+
+ var trashed = info.Element("Trashed").ValueOrDefault(false);
+ var restoreParent = info.Element("Trashed")?.Attribute("Parent").ValueOrDefault(Guid.Empty) ?? Guid.Empty;
+ return await HandleTrashedState(item, trashed, restoreParent, relationAlias);
+ }
+
+ [Obsolete("Use HandleTrashedState with relationAlias, will be removed in v18")]
protected virtual uSyncChange? HandleTrashedState(TObject item, bool trashed, Guid restoreParent)
=> uSyncChange.NoChange($"Member/{item.Name}", item.Name ?? item.Id.ToString());
+ protected virtual Task HandleTrashedState(TObject item, bool trashed, Guid restoreParent, string relationAlias)
+ {
+ if (!trashed && item.Trashed)
+ {
+ // if the item is trashed, then the change of it's parent
+ // should restore it (as long as we do a move!)
+
+ var restoreParentId = GetRelationParentId(item, restoreParent, relationAlias);
+ MoveItem(item, restoreParentId);
+
+ // clean out any relations for this item (some versions of Umbraco don't do this on a Move)
+ CleanRelations(item, relationAlias);
+
+ return Task.FromResult(uSyncChange.Update("Restored", item.Name ?? item.Id.ToString(), "Recycle Bin", restoreParent.ToString()));
+
+ }
+ else if (trashed && !item.Trashed)
+ {
+ // not already in the recycle bin?
+ if (item.ParentId > Constants.System.RecycleBinContent)
+ {
+ // clean any relations that may be there (stops an error)
+ CleanRelations(item, relationAlias);
+
+ // move to the recycle bin
+ MoveToRecycleBin(item);
+ }
+ else
+ {
+ // on first import the item might be in the recycle bin, but not marked as trash.
+ // but one does not simple set 'trashed' on a content item.
+ SetTrashed(item);
+
+ AddRelation(relationAlias, restoreParent, item.Id);
+ }
+
+ return Task.FromResult(uSyncChange.Update("Moved to Bin", item.Name ?? item.Id.ToString(), "", "Recycle Bin"));
+ }
+
+ return Task.FromResult(null);
+
+ }
+
+
protected async Task GetExportValueAsync(object? value, IPropertyType propertyType, string culture, string segment)
{
if (value is null) return string.Empty;
@@ -1012,7 +1072,19 @@ protected void CleanRelations(TObject item, string relationType)
{
logger.LogWarning(exception, "Error cleaning up relations: {id}", item.Id);
}
+ }
+
+ ///
+ /// Adds a relation for the given item. If the relation already exists, it is not added again.
+ ///
+ private void AddRelation(string relationAlias, Guid parentKey, int childId)
+ {
+ var existing = relationService.GetByChildId(childId, relationAlias);
+ if (existing.Any()) return;
+ var parent = GetByKey(parentKey);
+ if (parent != null && childId > 0)
+ relationService.Relate(parent.Id, childId, relationAlias);
}
protected int GetRelationParentId(TObject item, Guid restoreParentKey, string relationType)
@@ -1041,8 +1113,8 @@ protected int GetRelationParentId(TObject item, Guid restoreParentKey, string re
private List GetExcludedProperties(SyncSerializerOptions options)
{
List exclude = [.. dontSerialize];
-
- var excludeOptions = options.GetSetting(uSyncConstants.DefaultSettings.DoNotSerialize,
+
+ var excludeOptions = options.GetSetting(uSyncConstants.DefaultSettings.DoNotSerialize,
uSyncConstants.DefaultSettings.DoNotSerialize_Default);
if (!string.IsNullOrWhiteSpace(excludeOptions))
@@ -1065,7 +1137,7 @@ private List GetExcludedProperties(SyncSerializerOptions options)
}
catch (ArgumentException ex)
{
- logger.LogDebug("Unable to parse pattern '{pattern}' from '{settingsKey}' as Regex. {error}. Pattern will not be considered.", pattern,
+ logger.LogDebug("Unable to parse pattern '{pattern}' from '{settingsKey}' as Regex. {error}. Pattern will not be considered.", pattern,
uSyncConstants.DefaultSettings.DoNotSerializePattern, ex.Message);
return null;
}
diff --git a/uSync.Core/Serialization/Serializers/MediaSerializer.cs b/uSync.Core/Serialization/Serializers/MediaSerializer.cs
index 5d24ca8b4..14937da7d 100644
--- a/uSync.Core/Serialization/Serializers/MediaSerializer.cs
+++ b/uSync.Core/Serialization/Serializers/MediaSerializer.cs
@@ -59,19 +59,13 @@ protected override async Task> DeserializeCoreAsync(XElement
details.AddRange(await DeserializeBaseAsync(item, node, options));
- var info = node.Element(uSyncConstants.Xml.Info);
- if (info is not null)
- {
- var trashed = info.Element("Trashed").ValueOrDefault(false);
- var restoreParent = info.Element("Trashed")?.Attribute("Parent").ValueOrDefault(Guid.Empty) ?? Guid.Empty;
- details.AddNotNull(HandleTrashedState(item, trashed, restoreParent));
- }
-
var propertyAttempt = await DeserializePropertiesAsync(item, node, options);
if (!propertyAttempt.Success)
return SyncAttempt.Fail(item.Name ?? item.Id.ToString(), item, ChangeType.Fail, "Failed to save properties",
propertyAttempt.Exception ?? new Exception($"Error with properties {item.Id}"));
+ var info = node.Element(uSyncConstants.Xml.Info);
+
if (!options.GetSetting(uSyncConstants.DefaultSettings.IgnoreSortOrder, uSyncConstants.DefaultSettings.IgnoreSortOrder_Default))
{
var sortOrder = info?.Element(uSyncConstants.Xml.SortOrder).ValueOrDefault(-1) ?? -1;
@@ -103,33 +97,20 @@ protected override async Task> DeserializeCoreAsync(XElement
return SyncAttempt.Succeed(item.Name ?? item.Id.ToString(), item, ChangeType.Import, "", true, propertyAttempt.Result);
}
- protected override uSyncChange? HandleTrashedState(IMedia item, bool trashed, Guid restoreParentKey)
+ public override async Task> DeserializeSecondPassAsync(IMedia item, XElement node, SyncSerializerOptions options)
{
- if (!trashed && item.Trashed)
- {
- // if the item is trashed, then moving it back to the parent value
- // restores it.
-
- var restoreParentId = GetRelationParentId(item, restoreParentKey, Constants.Conventions.RelationTypes.RelateParentMediaFolderOnDeleteAlias);
- _mediaService.Move(item, restoreParentId);
-
- CleanRelations(item, Constants.Conventions.RelationTypes.RelateParentMediaFolderOnDeleteAlias);
-
- return uSyncChange.Update("Restored", item.Name ?? item.Id.ToString(), "Recycle Bin", item.ParentId.ToString());
- }
- else if (trashed && !item.Trashed)
- {
- // clean any rouge relations
- CleanRelations(item, Constants.Conventions.RelationTypes.RelateParentMediaFolderOnDeleteAlias);
-
- // move to the recycle bin
- _mediaService.MoveToRecycleBin(item);
- return uSyncChange.Update("Moved to Bin", item.Name ?? item.Id.ToString(), "", "Recycle Bin");
- }
+ var details = new List();
+ details.AddNotNull(await DeserializeTrashed(node, item, Constants.Conventions.RelationTypes.RelateParentMediaFolderOnDeleteAlias));
- return null;
+ return SyncAttempt.Succeed(item.Name ?? item.Id.ToString(), item,
+ details.Count == 0 ? ChangeType.NoChange : ChangeType.Import, details);
}
+ protected override void MoveToRecycleBin(IMedia item) => _mediaService.MoveToRecycleBin(item);
+ protected override void SetTrashed(IMedia item) => ((ContentBase)item).Trashed = true;
+ protected override void MoveItem(IMedia item, int parentId) => _mediaService.Move(item, parentId);
+ protected override IMedia? GetByKey(Guid id) => _mediaService.GetById(id);
+
protected override async Task> SerializeCoreAsync(IMedia item, SyncSerializerOptions options)
{
var node = InitializeNode(item, item.ContentType.Alias, options);