diff --git a/uSync.BackOffice.Targets/appsettings-schema.usync.json b/uSync.BackOffice.Targets/appsettings-schema.usync.json index e539eee12..c5acb1c13 100644 --- a/uSync.BackOffice.Targets/appsettings-schema.usync.json +++ b/uSync.BackOffice.Targets/appsettings-schema.usync.json @@ -205,7 +205,7 @@ }, "DisableNotificationSuppression": { "type": "boolean", - "description": "turns of use of the Notifications.Supress method, so notifications\nfire after every item is imported.\n ", + "description": "turns off use of the Notifications.Suppress method, so notifications\nfire after every item is imported.\n ", "default": "true" }, "BackgroundNotifications": { @@ -217,9 +217,37 @@ "type": "boolean", "description": "Move the uSync tree to it's own section in the back office. \n(requires a restart to take effect).\n ", "default": false + }, + "FolderMode": { + "description": "What type of mode the folder should work in (default, root, or production)\n ", + "default": "Normal", + "oneOf": [ + { + "$ref": "#/definitions/USyncBackOfficeConfigurationSyncFolderMode" + } + ] + }, + "ProductionFolder": { + "type": "string", + "description": "location of the 'production' folder to use when in production mode, \nor when creating the production mode files.\n ", + "default": "uSync/production" } } }, + "USyncBackOfficeConfigurationSyncFolderMode": { + "type": "string", + "description": "uSync's folder mode - normal, root or production\n ", + "x-enumNames": [ + "Normal", + "Root", + "Production" + ], + "enum": [ + "Normal", + "Root", + "Production" + ] + }, "USyncuSyncSetsDefinition": { "type": "object", "properties": { diff --git a/uSync.BackOffice/Configuration/SyncConfigService.cs b/uSync.BackOffice/Configuration/SyncConfigService.cs index a142000ad..e1377ec8e 100644 --- a/uSync.BackOffice/Configuration/SyncConfigService.cs +++ b/uSync.BackOffice/Configuration/SyncConfigService.cs @@ -66,11 +66,8 @@ private class SyncFolderItem /// public string GetWorkingFolder() { - var folders = FetchFolders(); - - return Settings.IsRootSite - ? folders[0].TrimStart('/') - : folders.Last().TrimStart('/'); + var folders = GetFolders(); + return folders.Last().TrimStart('/'); } /// @@ -78,9 +75,15 @@ public string[] GetFolders() { var folders = FetchFolders(); - return Settings.IsRootSite - ? [folders[0].TrimStart('/')] - : [.. folders.Select(x => x.TrimStart('/'))]; + switch(Settings.FolderMode) + { + case SyncFolderMode.Root: + return [folders[0].TrimStart('/')]; + case SyncFolderMode.Production: + return [Settings.ProductionFolder.TrimStart('/')]; + default: + return [.. folders.Select(x => x.TrimStart('/'))]; + } } /// diff --git a/uSync.BackOffice/Configuration/uSyncSettings.cs b/uSync.BackOffice/Configuration/uSyncSettings.cs index c290fe195..08eb0c7c5 100644 --- a/uSync.BackOffice/Configuration/uSyncSettings.cs +++ b/uSync.BackOffice/Configuration/uSyncSettings.cs @@ -197,26 +197,22 @@ public class uSyncSettings public string HideAddOns { get; set; } = "licence"; /// - /// turns of use of the Notifications.Supress method, so notifications + /// turns off use of the Notifications.Suppress method, so notifications /// fire after every item is imported. /// /// - /// I am not sure this does what i think it does, it doesn't suppress - /// then fire at the end , it just suppresses them all. + /// this disables the internal uSync scope provider that delays all + /// non cancellable notifications until after the import is complete. /// - /// until we have had time to look at this , we will leave this as - /// disabled by default so all notification messages fire. + /// on v13 this is false, the import happens and then the notifications fire. /// - /// for v13 thius is fine, but for v14, grouping the notifications - /// can causes issues if something fails. - /// - /// So if a single content import fails then the whole batch doesn't - /// get published properly (so no content for you :( ) . + /// on v16 the default is true, because some of the notifications appear to + /// be closely coupled to the save/publish process, and if something goes + /// wrong in one item's import it can cause a cascade of failures across + /// everything that might have been imported along with it. /// - /// there might be something downlever we can do, but it likey means - /// lots of core investigation to find that, for now 'old' school - /// non suppressed notifications should be fine (if a little slower). - /// + /// if the notifications are not suppressed, then if an item fails to import + /// it doesn't stop other items from being imported. /// [DefaultValue("true")] public bool DisableNotificationSuppression { get; set; } = true; @@ -238,4 +234,39 @@ public class uSyncSettings /// [DefaultValue(false)] public bool MoveToSection { get; set; } = false; + + /// + /// What type of mode the folder should work in (default, root, or production) + /// + [DefaultValue(SyncFolderMode.Normal)] + public SyncFolderMode FolderMode { get; set; } = SyncFolderMode.Normal; + + /// + /// location of the 'production' folder to use when in production mode, + /// or when creating the production mode files. + /// + [DefaultValue("uSync/production")] + public string ProductionFolder { get; set; } = "uSync/production"; } + +/// +/// uSync's folder mode - normal, root or production +/// +public enum SyncFolderMode +{ + /// + /// normal - expects individual files in the uSync folder(s) + /// + Normal, + + /// + /// root - will read and write things to the root folder, + /// + Root, + + /// + /// production - looks in a 'production' folder, expects a single file per handler. + /// + Production, +}; + diff --git a/uSync.BackOffice/Hubs/HubClientService.cs b/uSync.BackOffice/Hubs/HubClientService.cs index 4ec28edca..e39cd2836 100644 --- a/uSync.BackOffice/Hubs/HubClientService.cs +++ b/uSync.BackOffice/Hubs/HubClientService.cs @@ -80,5 +80,27 @@ public void PostUpdate(string message, int count, int total) /// get the uSync callbacks for this connection /// /// - public uSyncCallbacks Callbacks() => new(this.PostSummary, this.PostUpdate); + public uSyncCallbacks Callbacks() => new(this.PostSummary, this.PostUpdate, this.SetCountRange, this.PostIncrementalUpdate); + + private int _start = 0; + private int _end = 0; + + /// + /// set a range (start to end) that we expect the next set of updates to be bound within. + /// + public void SetCountRange(int start, int end) + { + _start = start; + _end = end; + } + + /// + /// post an update and increment the counter by one. + /// + public void PostIncrementalUpdate(string message) + { + _start++; + if (_start > _end) _end = _start; + this.PostUpdate(message, _start, _end); + } } diff --git a/uSync.BackOffice/Hubs/uSyncCallbacks.cs b/uSync.BackOffice/Hubs/uSyncCallbacks.cs index baddc009e..fc94fc782 100644 --- a/uSync.BackOffice/Hubs/uSyncCallbacks.cs +++ b/uSync.BackOffice/Hubs/uSyncCallbacks.cs @@ -1,7 +1,30 @@ -using uSync.BackOffice.SyncHandlers.Interfaces; +using uSync.BackOffice.Models; +using uSync.BackOffice.SyncHandlers.Interfaces; namespace uSync.BackOffice; +/// +/// Callback event for SignalR hub +/// +public delegate void SyncEventCallback(SyncProgressSummary summary); + +/// +/// callback delegate for SignalR messaging +/// +public delegate void SyncUpdateCallback(string message, int count, int total); + +/// +/// callback delegate to set the start and end range for the update counters. +/// +public delegate void SyncSetUpdateRange(int start, int end); + +/// +/// callback to send a update message and increment the counter by one so moving the progress bar. +/// +/// +public delegate void SyncIncrementalUpdateCallback(string message); + + /// /// Callback objects used to communicate via SignalR /// @@ -17,6 +40,16 @@ public class uSyncCallbacks /// public SyncUpdateCallback? Update { get; private set; } + /// + /// set a start and end range for the counter. + /// + public SyncSetUpdateRange? SetRange { get; private set; } + + /// + /// update and increment callback. + /// + public SyncIncrementalUpdateCallback? IncrementalUpdate { get; private set; } + /// /// generate a new callback object /// @@ -25,4 +58,14 @@ public uSyncCallbacks(SyncEventCallback? callback, SyncUpdateCallback? update) this.Callback = callback; this.Update = update; } + + /// + /// generate a callback object with range and incremental update + /// + public uSyncCallbacks(SyncEventCallback? callback, SyncUpdateCallback? update, SyncSetUpdateRange? updateRange, SyncIncrementalUpdateCallback incrementalUpdate) + : this(callback, update) + { + this.SetRange = updateRange; + this.IncrementalUpdate = incrementalUpdate; + } } diff --git a/uSync.BackOffice/Services/ISyncFileService.cs b/uSync.BackOffice/Services/ISyncFileService.cs index 40c12c12b..839886777 100644 --- a/uSync.BackOffice/Services/ISyncFileService.cs +++ b/uSync.BackOffice/Services/ISyncFileService.cs @@ -135,6 +135,11 @@ public interface ISyncFileService /// Task LoadXElementAsync(string file); + /// + /// merge all the files in the given folders into a single xml node, that can be bulk imported + /// + Task MakeSingleExportFromFolders(string[] folders, string itemType, ISyncTrackerBase? trackerBase, string fileName, string extension); + /// /// merge a list of files into a single XElement /// diff --git a/uSync.BackOffice/Services/ISyncService.cs b/uSync.BackOffice/Services/ISyncService.cs index a962c400e..9ccce0492 100644 --- a/uSync.BackOffice/Services/ISyncService.cs +++ b/uSync.BackOffice/Services/ISyncService.cs @@ -156,4 +156,10 @@ public interface ISyncService /// trigger the end of the bulk process /// Task FinishBulkProcessAsync(HandlerActions action, IEnumerable actions); + + /// + /// merge the given folders in single 'production' files for each handler. + /// + Task MergeExportFolder(string[] paths, IEnumerable handlers); + } \ No newline at end of file diff --git a/uSync.BackOffice/Services/SyncFileService.cs b/uSync.BackOffice/Services/SyncFileService.cs index bdf7dd3ca..9d79aa015 100644 --- a/uSync.BackOffice/Services/SyncFileService.cs +++ b/uSync.BackOffice/Services/SyncFileService.cs @@ -242,6 +242,7 @@ public async Task SaveFileAsync(string filename, string content) { CheckCharacters = false, Async = true, + IgnoreWhitespace = true, }; private static XmlWriterSettings _writerSettings = new XmlWriterSettings @@ -251,8 +252,6 @@ public async Task SaveFileAsync(string filename, string content) Async = true, CloseOutput= false, Indent = true, - - }; /// @@ -323,7 +322,26 @@ public void CopyFolder(string source, string target) { File.Copy(file, file.Replace(resolvedSource, resolvedTarget), true); } + } + + public async Task MakeSingleExportFromFolders(string[] folders, string itemType, ISyncTrackerBase? trackerBase, string filename, string extension) + { + var merged = await MergeFoldersAsync(folders, extension, trackerBase); + + var megaNode = new XElement(itemType + "s"); + int count = 0; + foreach(var item in merged) + { + count++; + megaNode.Add(new XElement(item.Node)); + } + + var resolvedTargetFile = GetAbsPath(filename); + CreateFoldersForFile(resolvedTargetFile); + + await SaveXElementAsync(megaNode, resolvedTargetFile); + return count; } /// diff --git a/uSync.BackOffice/Services/SyncService.cs b/uSync.BackOffice/Services/SyncService.cs index 0efa3a9d6..6332db6be 100644 --- a/uSync.BackOffice/Services/SyncService.cs +++ b/uSync.BackOffice/Services/SyncService.cs @@ -27,12 +27,6 @@ namespace uSync.BackOffice; - -/// -/// Callback event for SignalR hub -/// -public delegate void SyncEventCallback(SyncProgressSummary summary); - /// /// the service that does all the processing, /// this forms the entry point as an API to diff --git a/uSync.BackOffice/Services/SyncService_Files.cs b/uSync.BackOffice/Services/SyncService_Files.cs index d4842ebd5..1d23577be 100644 --- a/uSync.BackOffice/Services/SyncService_Files.cs +++ b/uSync.BackOffice/Services/SyncService_Files.cs @@ -1,7 +1,13 @@ -using System; +using Microsoft.Extensions.Logging; + +using System; +using System.Collections.Generic; using System.IO; using System.IO.Compression; using System.Linq; +using System.Threading.Tasks; + +using uSync.BackOffice.SyncHandlers.Models; namespace uSync.BackOffice; @@ -104,4 +110,29 @@ private static string CleanPathForZip(string path) => Path.GetFullPath( path.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar)) .TrimEnd(Path.DirectorySeparatorChar); + + /// + public async Task MergeExportFolder(string[] paths, IEnumerable handlers) + { + var totalMerged = 0; + + foreach (var handler in handlers) + { + var serializerType = handler.Handler.GetSerializerType(); + var baseTracker = handler.Handler.GetBaseTracker(); + if (serializerType is null || baseTracker is null) + { + _logger.LogWarning("Handler {Handler} does not support file merging", handler.Handler.Alias); + continue; + } + + var folders = paths.Select(x => Path.Combine(x, handler.Handler.DefaultFolder)).ToArray(); + var targetFileName = Path.Combine(_uSyncConfig.Settings.ProductionFolder, + handler.Handler.DefaultFolder + "." + _uSyncConfig.Settings.DefaultExtension); + + totalMerged += await _syncFileService.MakeSingleExportFromFolders(folders, serializerType, baseTracker, targetFileName, _uSyncConfig.Settings.DefaultExtension); + } + + return totalMerged; + } } diff --git a/uSync.BackOffice/Services/SyncService_Handlers.cs b/uSync.BackOffice/Services/SyncService_Handlers.cs index 6d204e05f..99958a7f7 100644 --- a/uSync.BackOffice/Services/SyncService_Handlers.cs +++ b/uSync.BackOffice/Services/SyncService_Handlers.cs @@ -38,9 +38,19 @@ public async Task> ReportHandlerAsync(string handler, u if (handlerPair == null) return []; var folders = GetHandlerFolders(GetFolderFromOptions(options), handlerPair.Handler); + var productionFile = $"{folders.Last()}.{_uSyncConfig.Settings.DefaultExtension}"; + if (_syncFileService.FileExists(productionFile)) + return await ReportMergedFile(productionFile, handlerPair, options); + return await handlerPair.Handler.ReportAsync(folders, handlerPair.Settings, options.Callbacks?.Update); } + private async Task> ReportMergedFile(string filename, HandlerConfigPair handlerPair, uSyncImportOptions options) + { + var node = await _syncFileService.LoadXElementAsync(filename); + return await handlerPair.Handler.ReportElementAsync(node, filename, handlerPair.Settings, options); + } + /// > public async Task> ImportHandlerAsync(string handlerAlias, uSyncImportOptions options) { @@ -68,12 +78,15 @@ public async Task> ImportHandlerAsync(string handlerAli backgroundTaskQueue: _backgroundTaskQueue, options.Callbacks?.Update); - var results = await handlerPair.Handler.ImportAllAsync(folders, handlerPair.Settings, options); + List results; - // _logger.LogDebug("< Import Handler {handler}", handlerAlias); + var productionFile = $"{folders.Last()}.{_uSyncConfig.Settings.DefaultExtension}"; + if (_syncFileService.FileExists(productionFile)) + results = [.. await ImportMergedFile(productionFile, handlerPair, options)]; + else + results = [.. await handlerPair.Handler.ImportAllAsync(folders, handlerPair.Settings, options)]; scope?.Complete(); - return results; } } @@ -83,6 +96,12 @@ public async Task> ImportHandlerAsync(string handlerAli } } + private async Task> ImportMergedFile(string filename, HandlerConfigPair handlerPair, uSyncImportOptions options) + { + var node = await _syncFileService.LoadXElementAsync(filename); + return await handlerPair.Handler.ImportElementAsync(node, filename, handlerPair.Settings, options); + } + /// > public async Task> PerformPostImportAsync(string[] folders, string handlerSet, IEnumerable actions) { diff --git a/uSync.BackOffice/SyncHandlers/Handlers/DataTypeHandler.cs b/uSync.BackOffice/SyncHandlers/Handlers/DataTypeHandler.cs index b58ae9cd9..6576b964d 100644 --- a/uSync.BackOffice/SyncHandlers/Handlers/DataTypeHandler.cs +++ b/uSync.BackOffice/SyncHandlers/Handlers/DataTypeHandler.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using System.Xml.XPath; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Cache; @@ -66,40 +67,7 @@ public DataTypeHandler( _dataTypeContainerService = dataTypeContainerService; } - /// - /// Process all DataType actions at the end of the import process - /// - /// - /// Datatypes have to exist early on so DocumentTypes can reference them, but - /// some doctypes reference content or document types, so we re-process them - /// at the end of the import process to ensure those settings can be made too. - /// - /// HOWEVER: The above isn't a problem Umbraco 10+ - the references can be set - /// before the actual doctypes exist, so we can do that in one pass. - /// - /// HOWEVER: If we move deletes to the end , we still need to process them. - /// but deletes are always 'change' = 'Hidden', so we only process hidden changes - /// - public override async Task> ProcessPostImportAsync(IEnumerable actions, HandlerSettings config) - { - if (actions == null || !actions.Any()) return []; - - var results = new List(); - var options = new uSyncImportOptions { Flags = SerializerFlags.LastPass }; - - // we only do deletes here. - foreach (var action in actions.Where(x => x.Change == ChangeType.Hidden)) - { - if (action.FileName is null) continue; - results.AddRange( - await ImportAsync(action.FileName, config, options)); - } - - results.AddRange(await CleanFoldersAsync(Guid.Empty)); - - return results; - } - + /// /// Fetch a DataType Container from the DataTypeService /// diff --git a/uSync.BackOffice/SyncHandlers/Interfaces/ISyncHandler.cs b/uSync.BackOffice/SyncHandlers/Interfaces/ISyncHandler.cs index 9bae83a8b..8450f5154 100644 --- a/uSync.BackOffice/SyncHandlers/Interfaces/ISyncHandler.cs +++ b/uSync.BackOffice/SyncHandlers/Interfaces/ISyncHandler.cs @@ -10,19 +10,25 @@ using uSync.Core; using uSync.Core.Dependency; using uSync.Core.Models; +using uSync.Core.Tracking; namespace uSync.BackOffice.SyncHandlers.Interfaces; -/// -/// callback delegate for SignalR messaging -/// -public delegate void SyncUpdateCallback(string message, int count, int total); - /// /// Handler interface for anything that wants to process elements via uSync /// public interface ISyncHandler { + /// + /// get the serializer type for the handler (e.g the name used in the xml) + /// + string? GetSerializerType() => null; + + /// + /// gets the base tracker from the serializer (used to track changes, merge items). + /// + ISyncTrackerBase? GetBaseTracker() => null; + /// /// alias for handler, used when finding a handler /// diff --git a/uSync.BackOffice/SyncHandlers/SyncHandlerContainerBase.cs b/uSync.BackOffice/SyncHandlers/SyncHandlerContainerBase.cs index 26a773b2c..4b76fb953 100644 --- a/uSync.BackOffice/SyncHandlers/SyncHandlerContainerBase.cs +++ b/uSync.BackOffice/SyncHandlers/SyncHandlerContainerBase.cs @@ -7,6 +7,7 @@ using System.Threading; using System.Threading.Tasks; using System.Xml.Linq; +using System.Xml.XPath; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Cache; @@ -24,6 +25,8 @@ using uSync.Core.Dependency; using uSync.Core.Serialization; +using CoreConstants = uSync.Core.uSyncConstants; + namespace uSync.BackOffice.SyncHandlers; /// @@ -111,13 +114,39 @@ public virtual async Task> ProcessPostImportAsync(IEnum var results = new List(); var options = new uSyncImportOptions { Flags = SerializerFlags.LastPass }; + // a cache of loaded files so we don't keep loading the same file multiple times + // in production mode the same file might contain multiple 'empty' nodes + // and they can be large, and require loading from disk multiple times would slow + // it all down. + Dictionary _loadedFiles = []; + // we only do deletes here. foreach (var action in actions.Where(x => x.Change == ChangeType.Hidden)) { if (action.FileName is null) continue; - results.AddRange(await ImportAsync(action.FileName, config, options)); + if (syncFileService.FileExists(action.FileName) is false) continue; + + // load the file if we haven't already + if (_loadedFiles.TryGetValue(action.FileName, out XElement? xml) is false) + _loadedFiles[action.FileName] = await syncFileService.LoadXElementAsync(action.FileName); + + if (_loadedFiles[action.FileName].Name.LocalName.Equals(CoreConstants.Serialization.Empty) is true) + { + // single + results.AddRange(await ImportSingleElementAsync(_loadedFiles[action.FileName], action.FileName, config, options)); + } + else + { + // multiple ? + var node = _loadedFiles[action.FileName].XPathSelectElement($"//{CoreConstants.Serialization.Empty}[@Key='{action.Key}']"); + if (node is null) continue; + + results.AddRange(await ImportSingleElementAsync(node, action.FileName, config, options)); + } } + _loadedFiles.Clear(); + results.AddRange(await CleanFoldersAsync(Guid.Empty)); return results; diff --git a/uSync.BackOffice/SyncHandlers/SyncHandlerRoot.cs b/uSync.BackOffice/SyncHandlers/SyncHandlerRoot.cs index 05aaafc53..cdfaac4b8 100644 --- a/uSync.BackOffice/SyncHandlers/SyncHandlerRoot.cs +++ b/uSync.BackOffice/SyncHandlers/SyncHandlerRoot.cs @@ -169,6 +169,16 @@ public abstract class SyncHandlerRoot /// protected readonly IShortStringHelper shortStringHelper; + /// + /// The serializer's item type (this is what the xml-node name will be). + /// + public string? GetSerializerType() => serializer.ItemType; + + /// + /// tracker used by the serializer. + /// + public ISyncTrackerBase? GetBaseTracker() => trackers.FirstOrDefault() as ISyncTrackerBase; + /// /// Constructor, base for all handlers /// @@ -268,11 +278,12 @@ public async Task> ImportAllAsync(string[] folders, Han int count = 0; int total = items.Count; + options.Callbacks?.SetRange?.Invoke(count, total); + foreach (var item in items) { count++; - options.Callbacks?.Update?.Invoke($"Importing {Path.GetFileNameWithoutExtension(item.Path)}", count, total); var result = await ImportElementAsync(item.Node, item.FileName, config, options); foreach (var attempt in result) @@ -435,13 +446,32 @@ virtual public async Task> ImportAsync(string file, Han return await ImportAsync(file, config, options); } + /// + virtual public async Task> ImportElementAsync(XElement node, string filename, HandlerSettings settings, uSyncImportOptions options) + { + if (node.Name.LocalName == this.serializer.ItemType + "s") + { + var actions = new List(); + var elements = node.Elements().ToList(); + options.Callbacks?.SetRange?.Invoke(0, elements.Count); + foreach (var item in elements) + { + actions.AddRange(await ImportSingleElementAsync(new XElement(item), filename, settings, options)); + } + return actions; + } + + return await ImportSingleElementAsync(node, filename, settings, options); + } + /// - /// Import a node, with settings and options + /// import a single XElement into umbraco. /// /// - /// All Imports lead here + /// if the XElement contains multiple entries, then this method will not import them, if there is a possibility of that + /// then the ImportElementAsync method should be used - which splits them before loading this call. /// - virtual public async Task> ImportElementAsync(XElement node, string filename, HandlerSettings settings, uSyncImportOptions options) + virtual protected async Task> ImportSingleElementAsync(XElement node, string filename, HandlerSettings settings, uSyncImportOptions options) { if (!await ShouldImportAsync(node, settings)) { @@ -459,6 +489,8 @@ virtual public async Task> ImportElementAsync(XElement try { + options.Callbacks?.IncrementalUpdate?.Invoke(node.GetAlias()); + // merge the options from the handler and any import options into our serializer options. var serializerOptions = new SyncSerializerOptions(options.Flags, settings.Settings, options.UserId); serializerOptions.MergeSettings(options.Settings); @@ -1191,9 +1223,28 @@ protected virtual async Task> ReportFolderAsync(string } /// - /// Report on any changes for a single XML node. + /// Report on any changes for a single XML node. (may contain multiple items in a single node). /// public virtual async Task> ReportElementAsync(XElement node, string filename, HandlerSettings settings, uSyncImportOptions options) + { + if (node.Name.LocalName == this.serializer.ItemType + "s") + { + var actions = new List(); + foreach (var item in node.Elements()) + { + var cleanItem = new XElement(item); + actions.AddRange(await ReportElementSingleAsync(cleanItem, filename, settings, options)); + } + return actions; + } + + return await ReportElementSingleAsync(node, filename, settings, options); + } + + /// + /// Report on any changes for a single XML node. + /// + public virtual async Task> ReportElementSingleAsync(XElement node, string filename, HandlerSettings settings, uSyncImportOptions options) { try { diff --git a/uSync.BackOffice/uSyncBackOfficeBuilderExtensions.cs b/uSync.BackOffice/uSyncBackOfficeBuilderExtensions.cs index c5c819bbc..eefa99efe 100644 --- a/uSync.BackOffice/uSyncBackOfficeBuilderExtensions.cs +++ b/uSync.BackOffice/uSyncBackOfficeBuilderExtensions.cs @@ -103,6 +103,9 @@ public static IUmbracoBuilder AdduSync(this IUmbracoBuilder builder, Action(StatusCodes.Status200OK)] + public async Task MergeExportFolder() + { + var folders = _configService.GetFolders(); + var handlers = _handlerFactory.GetValidHandlers(new BackOffice.SyncHandlers.Models.SyncHandlerOptions { Set = _configService.Settings.DefaultSet }); + var result = await _syncService.MergeExportFolder(folders, handlers); + return Ok(result); + } +} diff --git a/uSync.Backoffice.Management.Api/Controllers/Actions/uSyncActionsController.cs b/uSync.Backoffice.Management.Api/Controllers/Actions/uSyncActionsController.cs index 0de5a0a02..b4bfb55ae 100644 --- a/uSync.Backoffice.Management.Api/Controllers/Actions/uSyncActionsController.cs +++ b/uSync.Backoffice.Management.Api/Controllers/Actions/uSyncActionsController.cs @@ -33,7 +33,7 @@ public async Task> GetActions() public async Task> GetActionsBySet(string setName) { return await Task.FromResult(_syncManagementService.GetActions(setName)); - } + } } diff --git a/uSync.Backoffice.Management.Client/usync-assets/src/workspace/views/settings/settings.element.ts b/uSync.Backoffice.Management.Client/usync-assets/src/workspace/views/settings/settings.element.ts index 80767a0bd..b3be1d3c7 100644 --- a/uSync.Backoffice.Management.Client/usync-assets/src/workspace/views/settings/settings.element.ts +++ b/uSync.Backoffice.Management.Client/usync-assets/src/workspace/views/settings/settings.element.ts @@ -60,7 +60,7 @@ export class USyncSettingsViewElement extends UmbElementMixin(LitElement) { .value=${this.settings?.exportAtStartup}> diff --git a/uSync.Core/Serialization/SyncSerializerRoot.cs b/uSync.Core/Serialization/SyncSerializerRoot.cs index cfac15cd2..017225de8 100644 --- a/uSync.Core/Serialization/SyncSerializerRoot.cs +++ b/uSync.Core/Serialization/SyncSerializerRoot.cs @@ -281,7 +281,7 @@ public virtual Task> SerializeEmptyAsync(TObject item, Syn var node = XElementExtensions.MakeEmpty(ItemKey(item), change, alias); - return Task.FromResult(SyncAttempt.Succeed("Empty", node, ChangeType.Removed, [])); + return Task.FromResult(SyncAttempt.Succeed(uSyncConstants.Serialization.Empty, node, ChangeType.Removed, [])); } diff --git a/uSync.Core/uSyncConstants.cs b/uSync.Core/uSyncConstants.cs index bf84c7661..3057ba3d6 100644 --- a/uSync.Core/uSyncConstants.cs +++ b/uSync.Core/uSyncConstants.cs @@ -49,6 +49,9 @@ public static class Serialization public const string Domain = "Domain"; + /// + /// action files are 'empty' with an action to say what they do. + /// public const string Empty = "Empty"; public const string RelationType = "RelationType";