Try our conversational search powered by Generative AI!

Le Giang
Mar 1, 2018
  3822
(6 votes)

Single copy and paste in PageNavigationTree

By the default, when you copy and paste a page in PageNavigationTree (PNT), all its children also come along. Recently a discussion on Yammer has raised question about how to just copy and paste parent page without its children. Currently edit mode UI does not support this function, so in this post I would like to describe the way to do this.

  1. Firstly, we need a new command called "Copy this page" in the tree's context menu. It works same as the default Copy command except that it sets a flag "copyMode" to "single". We also have to override the default Copy and Paste commands to set and send this flag to the server side for further processing. 
    //CopySingleContent
    define([
        "dojo/_base/declare",
        "epi-cms/command/CopyContent"
    ], function (declare, CopyContent) {
    
        return declare([CopyContent], {
            
            label: "Copy this page",        
            iconClass: "epi-iconCopy",
            _execute: function () {
                this.clipboard.set("copyMode", true);
                this.inherited(arguments);
            }
        });
    });
    // PasteContent
    define([
        "dojo/_base/declare",
        "epi-cms/command/PasteContent",
        "epi-propertydemo/service/CustomContentHierarchyService"
    ], function (declare, PasteContent, CustomContentHierarchyService) {
    
        return declare([PasteContent], {
            _execute: function () {
                // summary:
                //      Override to handle "single copy"
                // tags:
                //      protected
    
                if (this.clipboard.copyMode === "single") {
                    this.model.service = new CustomContentHierarchyService();
                    this.model.service.singleCopy = true;
                }
                this.inherited(arguments);
            }
        });
    });
    // CopyContent
    define([
        "dojo/_base/declare",
        "epi-cms/command/CopyContent"
    ], function (declare, CopyContent)
    {
    
        return declare([CopyContent], {
            _execute: function() {
                // summary:
                //      Override to set copyMode to null
                // tags:
                //      protected
    
                this.clipboard.set("copyMode", null);
                this.inherited(arguments);
            }
        });
    });
  2. Now we need send the "copyMode" to the server. This is done in a custom ContentHierarchyService 
    define([
        "dojo/_base/declare",
        "dojo/_base/lang",
        "dojo/aspect",
        "epi/dependency",
        "epi-cms/contentediting/ContentHierarchyService"
    ], function (declare,lang, aspect, dependency, ContentHierarchyService) {
    
        return declare([ContentHierarchyService], {
    
            postscript: function (params) {
                this.inherited(arguments);
    
                aspect.before(this.store.xhrHandler, "xhr", lang.hitch(this, function (/*String*/method, /*dojo.__XhrArgs*/args, /*Boolean?*/hasBody) {
                    if (this.singleCopy && method == "POST") {
                        args.headers = args.headers || {};
                        args.headers = lang.mixin({}, args.headers, { "X-EpiCopyMode": "single" });
                    }
                    return [method, args];
                }));
    
                aspect.after(this.store.xhrHandler, "xhr", lang.hitch(this, function (deferred) {
                    this.singleCopy = null;
                    return deferred;
                }));
            }        
        });
    });

    We attach"X-EpiCopyMode" param to the request header, this will be consummed in the server-side to decide to perform "single copy" or "normal copy".

  3. We have created custom commands but they haven't been used anywhere. We need attach them to page tree's context menu. Unfortunately, there is no an easy way to do this, but the hard way. 
    define([
      "dojo/_base/declare",
      "dojo/_base/array",
      "dojo/aspect",
    
      "epi/_Module",
      "epi/dependency",
    
      "epi-cms/plugin-area/navigation-tree",
      "epi-cms/component/MainNavigationComponent",
      "epi-cms/command/PasteContent",  
      "epi-cms/command/CopyContent",
    
      "epi-propertydemo/command/CopySingleContent",
      "epi-propertydemo/command/PasteContent",
      "epi-propertydemo/command/CopyContent"
    ], function (
      declare,
      array,
      aspect,
    
      _Module,
      dependency,
    
      navigationTreePluginArea,
      MainNavigationComponent,
      PasteContent,
      CopyContent,
    
      CopySingleContent,
      CustomPasteContent,
      CustomCopyContent
    ) {
        return declare([_Module], {
    
          initialize: function () {
    
              this.inherited(arguments);
    
              var widgetFactory = dependency.resolve("epi.shell.widget.WidgetFactory");
              aspect.after(widgetFactory, "onWidgetCreated", function (widget, componentDefinition) {
                if (widget.isInstanceOf(MainNavigationComponent)) {                
                  aspect.after(widget, "own", function () {
                    var cmd = new CopySingleContent({
                        category: "context",
                        clipboard: this.tree._clipboardManager,
                        selection: this.tree.selection,
                        model: this.tree.model
                    });
                    navigationTreePluginArea.add(cmd);
    
                    var self = this;
                    aspect.after(this.tree._contextMenuCommandProvider, "get", function (commands) {
                      
                      for (var i = 0; i < commands.length; i++) {
                        var command = commands[i];
                        if (command.isInstanceOf(PasteContent)) {
                            commands[i] = new CustomPasteContent({
                                category: "context",
                                clipboard: self.tree._clipboardManager,
                                selection: self.tree.selection,
                                model: self.tree.model
                            });
                        }
                        if (command.isInstanceOf(CopyContent) && !command.isInstanceOf(CopySingleContent)) {
                            commands[i] = new CustomCopyContent({
                                category: "context",
                                clipboard: self.tree._clipboardManager,
                                selection: self.tree.selection,
                                model: self.tree.model
                            });
                        }
                      }
                      
                      return commands;
                    });                
                  });              
                }              
              }, true);         
          }
        });
      });
  4. We have completed client-side work, now is server-side. To decide to perform "single copy" or "normal copy", we need create an intercept for IContentCopyHandler. 
     public class ContentCopyHandlerIntercept : IContentCopyHandler
        {
            private readonly IContentCopyHandler _defaultHandler;
            public ContentCopyHandlerIntercept(IContentCopyHandler defaultHandler)
            {
                _defaultHandler = defaultHandler;
            }
    
            public ContentReference Copy(ContentReference contentLink, ContentReference destinationLink, AccessLevel requiredSourceAccess, bool publishOnDestination)
            {
                var singleCopy = HttpContext.Current.Request.Headers["X-EpiCopyMode"] == "single";
                if(!singleCopy)
                {
                    return _defaultHandler.Copy(contentLink, destinationLink, requiredSourceAccess, publishOnDestination);
                }
    
                var customContentCopyHandler = ServiceLocator.Current.GetInstance<SingleContentCopyHandler>();
                return customContentCopyHandler.Copy(contentLink, destinationLink, requiredSourceAccess, publishOnDestination);
            }
        }

    We need register this intercept at initialization time

    [ModuleDependency(typeof(Cms.Shell.InitializableModule))]
        public class InitializationModule : IConfigurableModule
        {
            public void ConfigureContainer(ServiceConfigurationContext context)
            {
                context.ConfigurationComplete += (o, e) =>
                {
                    context.Services.Intercept<IContentCopyHandler>((locator, defaultImplement) =>
                    {
                        return new ContentCopyHandlerIntercept(defaultImplement);
                    });
                };            
            }
    }
  5. Finally, we have to implement the SingleContentCopyHandler. 
    [ServiceConfiguration(typeof(IContentCopyHandler))]
        public class SingleContentCopyHandler : IContentCopyHandler
        {
            private class CopyContext
            {
                public CopyContext()
                {
                    ExportSources = new List<ExportSource>();
                }
    
                public IList<ExportSource> ExportSources { get; set; }
                public ContentReference DestinationRoot { get; set; }
                public ContentReference OriginalLink { get; set; }
                public ContentReference NewLink { get; set; }
                public bool PublishOnDestination { get; set; }
                public bool InBackground { get; set; }
                public int PageCount { get; set; }
                public String LanguageCode { get; set; }
                public AccessLevel RequiredSourceAccess { get; set; }
            }        
    
            public SingleContentCopyHandler(IContentRepository contentRepository,
                ServiceAccessor<IDataImporter> dataImporterAccessor, 
                ServiceAccessor<IDataExporter> dataExporterAccessor,
                ContentOptions contentOptions)
            {
                _contentRepository = contentRepository;
                _dataImporterAccessor = dataImporterAccessor;
                _dataExporterAccessor = dataExporterAccessor;
                _contentOptions = contentOptions;
            }
    
            private static readonly ILog _log = LogManager.GetLogger(typeof(ContentCopyHandler));
            private readonly IContentRepository _contentRepository;
            private readonly ServiceAccessor<IDataImporter> _dataImporterAccessor;
            private readonly ServiceAccessor<IDataExporter> _dataExporterAccessor;
            private readonly ContentOptions _contentOptions;
    
    
            #region IContentCopyHandler Members
    
            /// <summary>
            /// Copy pages to another container.
            /// </summary>
            /// <param name="pageLink">The link to the content data to copy.</param>
            /// <param name="destinationLink">The container where the page will be copied</param>
            /// <param name="requiredSourceAccess">The required source access to check access against</param>
            /// <param name="publishOnDestination">If the new pages should be published on the destination</param>
            /// <returns></returns>
            public virtual ContentReference Copy(ContentReference pageLink, ContentReference destinationLink, AccessLevel requiredSourceAccess, bool publishOnDestination)
            {
                CopyContext arguments = new CopyContext()
                {
                    NewLink = ContentReference.EmptyReference,
                    OriginalLink = pageLink,
                    DestinationRoot = destinationLink,
                    PublishOnDestination = publishOnDestination,
                    PageCount = 1,
                    LanguageCode = Thread.CurrentThread.CurrentUICulture.Name,
                    RequiredSourceAccess = requiredSourceAccess
                };
    
                arguments.ExportSources.Add(new ExportSource(arguments.OriginalLink, ExportSource.NonRecursive));
                ExecuteCopying(arguments);
    
                //Export:Review
                return arguments.NewLink;
            }
    
            #endregion
            
            private void ExecuteCopying(CopyContext context)
            {
                bool useFile = context.PageCount > _contentOptions.InMemoryCopyThreshold;
                string filePath = useFile ? Path.GetTempFileName() : null;
                try
                {
                    using (Stream stream = CreateStream(useFile, filePath))
                    {
                        using (var exporter = _dataExporterAccessor())
                        {
    
                            var options = new ExportOptions()
                            {
                                TransferType = TypeOfTransfer.Copying,
                                RequiredSourceAccess = context.RequiredSourceAccess,
                                AutoCloseStream = false,
                                ExcludeFiles = false
                            };
    
                            var exporterLog = exporter.Export(stream, context.ExportSources, options);
                            if (exporter.Status.Log.Errors.Count > 0)
                            {
                                Rollback(context, exporter.Status.Log);
                            }
                        }
    
                        stream.Seek(0, SeekOrigin.Begin);
    
                        var importer = _dataImporterAccessor();
                        var copyOptions = new ImportOptions()
                        {
                            TransferType = TypeOfTransfer.Copying,
                            EnsureContentNameUniqueness = true,
                            SaveAction = (context.PublishOnDestination ? SaveAction.Publish : SaveAction.CheckOut) | SaveAction.SkipValidation
                        };
    
                        var log = importer.Import(stream, context.DestinationRoot, copyOptions);
                        context.NewLink = importer.Status.ImportedRoot;
                        if (importer.Status.Log.Errors.Count > 0)
                        {
                            Rollback(context, importer.Status.Log);
                        }
                    }
                }
                finally
                {
                    if (File.Exists(filePath))
                    {
                        File.Delete(filePath);
                    }
                }
            }
    
            private static Stream CreateStream(bool useFile, string filePath)
            {
                if (useFile)
                {
                    return new FileStream(filePath, FileMode.Open, FileAccess.ReadWrite);
                }
                else
                {
                    return new MemoryStream();
                }
            }
    
            private void Rollback(CopyContext context, ITransferLogger transferLogger)
            {
                try
                {
                    if (!ContentReference.IsNullOrEmpty(context.NewLink))
                    {
                        _contentRepository.Delete(context.NewLink, true, EPiServer.Security.AccessLevel.NoAccess);
                    }
                }
                catch (Exception ex)
                {
                    transferLogger.Error(String.Format("Failed to rollback creation of '{0}'", context.NewLink.ToString()), ex);
                }
    
                if (_log.IsErrorEnabled)
                {
                    _log.ErrorFormat("Failed to copy pages with root '{0}' to '{1}'",
                        context.OriginalLink.ToString(), context.DestinationRoot.ToString());
                }
    
                String translatedMessage = LocalizationService.Current.GetStringByCulture("/copy/backgroundreport/failed", "Failed to copy content '{0}' to '{1}'", CultureInfo.GetCultureInfo(context.LanguageCode));
                String formattedMessage = String.Format(translatedMessage, _contentRepository.Get<IContent>(context.OriginalLink)?.Name, _contentRepository.Get<IContent>(context.DestinationRoot)?.Name);
                String errorMessage = String.Format("{0}:{1}", formattedMessage, String.Join(",", transferLogger.Errors.Cast<String>().ToArray<string>()));
                throw new EPiServerException(errorMessage);
            }
        }

    The main point here is the line : arguments.ExportSources.Add(new ExportSource(arguments.OriginalLink, ExportSource.NonRecursive)); It allows to copy just content without its children.

  6. And here is the result for a very long code:

Image TtDWKW6yLa.gif

Mar 01, 2018

Comments

Mar 1, 2018 10:31 AM

Nice work!

Kane Made It
Kane Made It Mar 1, 2018 11:23 AM

It's great for Admin and Editor. I just wish this would be implemented in our CMS as a feature.

Mar 1, 2018 08:27 PM

Great post! Any chance of packing this up as a Nuget package?

Mar 2, 2018 09:44 AM

Very nice, thanks for sharing this.

Mar 5, 2018 11:21 AM

I support Kane's proposal, make this as a feature in the CMS (now you have all the code as well - copy / paste).

Please login to comment.
Latest blogs
Headless forms reloaded (beta)

Forms is used on the vast majority of CMS installations. But using Forms in a headless setup is a bit of pain since the rendering pipeline is based...

MartinOttosen | Mar 1, 2024

Uploading blobs to Optimizely DXP via PowerShell

We had a client moving from an On-Prem v11 Optimizely instance to DXP v12 and we had a lot of blobs (over 40 GB) needing uploading to DXP as a part...

Nick Hamlin | Mar 1, 2024 | Syndicated blog

DbLocalizationProvider v8.0 Released

I’m pleased to announce that Localization Provider v8.0 is finally out.

valdis | Feb 28, 2024 | Syndicated blog

Epinova DXP deployment extension – With Octopus deploy

Example how you can use Epinova DXP deployment extension in Octopus deployment.

Ove Lartelius | Feb 28, 2024 | Syndicated blog

Identify Azure web app instance id's for an Optimizely CMS site

When running Optimizely CMS in Azure, you will be using an instance bound cloud license. What instances are counted, and how can you check them? Le...

Tomas Hensrud Gulla | Feb 27, 2024 | Syndicated blog

Introducing Image Transformer - AI Assistant for Optimizely

We've got something super cool to share with you, and it's all about giving your images a fresh spin. Image Transformer, the latest feature from ou...

Luc Gosso (MVP) | Feb 26, 2024 | Syndicated blog