Changes for page Nested Pages Migration
Last modified by Bart Vastenhouw on 2022/03/31 18:13
From version 2.1
edited by Bart Vastenhouw
on 2022/03/31 18:13
on 2022/03/31 18:13
Change comment:
Install extension [org.xwiki.contrib:application-nestedpagesmigrator-ui/0.8.2]
To version 1.1
edited by Bart Vastenhouw
on 2022/02/03 12:08
on 2022/02/03 12:08
Change comment:
Install extension [org.xwiki.contrib:application-nestedpagesmigrator-ui/0.7.3]
Summary
-
Page properties (2 modified, 0 added, 0 removed)
-
Objects (0 modified, 4 added, 1 removed)
Details
- Page properties
-
- Title
-
... ... @@ -1,0 +1,1 @@ 1 +Nested Pages Migration - Content
-
... ... @@ -1,0 +1,288 @@ 1 +{{velocity output="false"}} 2 +#************************************************************************ 3 + * Compute and return the maximum authorized length for the full name. 4 + ************************************************************************# 5 +#macro(getLocalReferenceMaxLength) 6 + #set ($localReferenceMaxLength = '255') 7 + ## Available since XWiki 11.4RC1. 8 + #if ($doc.localReferenceMaxLength) 9 + #set ($localReferenceMaxLength = $doc.localReferenceMaxLength) 10 + #end 11 + ## The document reference size limit was increased from 255 to 768 (the maximum supported by MySQL) in XWiki 13.2. 12 + #if ($services.extension.core.getCoreExtension('org.xwiki.platform:xwiki-platform-model').id.version.compareTo('13.2') >= 0) 13 + #set ($localReferenceMaxLength = $mathtool.sub($localReferenceMaxLength, $xcontext.database.length())) 14 + #else 15 + ## To avoid issues with documents with path too long, take some extra margin, higher than the wiki name length. 16 + #set ($localReferenceMaxLength = $mathtool.sub($localReferenceMaxLength, 50)) 17 + #end 18 + $localReferenceMaxLength 19 +#end 20 +{{/velocity}} 21 + 22 +{{velocity}} 23 +#if (!$services.security.authorization.hasAccess('admin', $xcontext.userReference, $doc.documentReference.wikiReference)) 24 + {{error}} 25 + You don't have the right to use this tool on this wiki. You need to be administrator. 26 + {{/error}} 27 +#else 28 +## Both job.css and extension.css are needed because the ui-progress classes that we need to display 29 +## a progress bar are in one of these 2 files depending on the XWiki version 30 +#set ($discard = $xwiki.ssfx.use('uicomponents/job/job.css', true)) 31 +#set ($discard = $xwiki.ssfx.use('uicomponents/extension/extension.css', true)) 32 +#set ($discard = $xwiki.ssfx.use('uicomponents/logging/logging.css', true)) 33 +#set ($discard = $xwiki.linkx.use($services.webjars.url('org.xwiki.platform:xwiki-platform-tree-webjar', 'tree.min.css', 34 + {'evaluate': true}), {'type': 'text/css', 'rel': 'stylesheet'})) 35 +{{html clean="false"}} 36 +<!------------------------------------------ 37 + Migration Action Template 38 + -------------------------------------------> 39 +<script id="MigrationActionTemplate" type="text/html"> 40 + <!-- ko foreach: ${escapetool.d}data.actions --> 41 + <li class="jstree-node" data-bind=" 42 + visible: (!targetDocument.equals(sourceDocument) || getNumberOfChildren() > 0 || getNumberOfPreferences() > 0 || getNumberOfRights()> 0), 43 + css: { 44 + 'jstree-closed': !displayChildren(), 45 + 'jstree-open' : displayChildren(), 46 + 'jstree-leaf' : getNumberOfChildren() == 0 && getNumberOfPreferences() == 0, 47 + 'jstree-last' : ${escapetool.d}index() == ${escapetool.d}parent.actions.length - 1 48 + }"> 49 + ## Display the tree branch 50 + <i class="jstree-icon jstree-ocl" role="presentation" data-bind="click: toggleDisplayChildren"></i> 51 + ## Display the checbox 52 + <input type="checkbox" data-bind="checked: enabled" /> 53 + ## Display the 'all' link 54 + <a href="#" data-bind="visible: !enabled() && (getNumberOfChildren() > 0 || getNumberOfPreferences() > 0 || getNumberOfRights()> 0), click: enableWithChildren" >(all)</a> 55 + ## Display the document name 56 + <strong class="documentName" data-bind="text: getTargetName(), click: toggleDisplayChildren, css: { 'bg-danger': isTooLong() }"></strong> 57 + ## Display the target location 58 + [<span data-bind="text: serializedTargetDocument()" class="monospace"></span>] 59 + ## Display if the action is a change or not 60 + <em data-bind="visible: targetDocument.equals(sourceDocument)">(unchanged)</em> 61 + ## Display if a previous document will be deleted 62 + <strong data-bind="visible: deletePrevious">(duplicated document will be deleted)</strong> 63 + ## Display the exclude page button 64 + <button class="btn btn-default btn-xs" data-bind="click: ${escapetool.d}root.excludePage, disable: targetDocument.equals(sourceDocument)">exclude page</button> 65 + ## Display the exclude space button 66 + <button class="btn btn-default btn-xs" data-bind="click: ${escapetool.d}root.excludeSpace">exclude space</button> 67 + ## Display th button to change the parent of ht document 68 + <button class="btn btn-default btn-xs" data-bind="click: ${escapetool.d}root.setParent">set parent</button> 69 + ## Display the number of children 70 + (<span data-bind="text: getNumberOfChildren()"></span> children, 71 + ## Display the number of preferences 72 + <span data-bind="text: getNumberOfPreferences()"></span> preferences, 73 + ## Display the number of rights 74 + <span data-bind="text: getNumberOfRights()"></span> rights) 75 + ## Display the original location 76 + from <a target="_blank" class="monospace" data-bind="text: serializedSourceDocument(), attr: {href: getSourceLink()}"></a> 77 + ## Display all children 78 + <!-- ko if: displayChildren() --> 79 + ## Display preferences 80 + <!-- ko if: preferences.length > 0 --> 81 + <ul data-bind="foreach: preferences" class="jstree-children"> 82 + <li class="text-warning jstree-node jstree-leaf"> 83 + <i class="jstree-icon jstree-ocl" role="presentation"></i> 84 + <input type="checkbox" data-bind="checked: enabled"/> <strong>[Preferences] <span data-bind="text: property"></span> : <span data-bind="text: value"></span></strong> (coming from <a target="_blank" class="monospace" data-bind="text: getSerializedOrigin(), attr: {href: getOriginLink()}"></a>) 85 + </li> 86 + </ul> 87 + <!-- /ko --> 88 + ## Display rights 89 + <!-- ko if: rights.length > 0 --> 90 + <ul data-bind="foreach: rights" class="jstree-children"> 91 + <li class="text-danger jstree-node jstree-leaf"> 92 + <i class="jstree-icon jstree-ocl" role="presentation"></i> 93 + <input type="checkbox" data-bind="checked: enabled"/> <strong>[Right] <span data-bind="text: toString()"></span></strong> (coming from <a target="_blank" class="monospace" data-bind="text: getSerializedOrigin(), attr: {href: getOriginLink()}"></a>) 94 + </li> 95 + </ul> 96 + <!-- /ko --> 97 + ## Display children documents 98 + <ul data-bind="template: { name: 'MigrationActionTemplate', data: {'actions': children()} }" class="jstree-children"/> 99 + <!-- /ko --> 100 + </li> 101 + <!-- /ko --> 102 +</script> 103 +<!------------------------------------------ 104 + Display Logs 105 + -------------------------------------------> 106 +<script id="DisplayLogs" type="text/html"> 107 + <h2 class="log-title">Logs: </h2> 108 + <ul class="log" data-bind="if: logs().length > 0"> 109 + <!-- ko foreach: logs --> 110 + <li class="log-item" data-bind="css: getClass()"> 111 + <div data-bind="text: message"></div> 112 + <!-- ko if: stackTrace --> 113 + <pre data-bind="text: stackTrace"></pre> 114 + <!-- /ko --> 115 + </li> 116 + <!-- /ko --> 117 + </ul> 118 +</script> 119 +<!------------------------------------------ 120 + Display plan 121 + -------------------------------------------> 122 +<script id="DisplayPlan" type="text/html"> 123 + <h2>Plan</h2> 124 + <div class="migration-plan box"> 125 + <div data-bind="if: isComputing()" id="planComputing"> 126 + <p>The plan is being computed and it could take some time. Please wait...</p> 127 + <div class="ui-progress-background"> 128 + <div class="ui-progress-bar green" data-bind="style: {width: progress() + '%'}"></div> 129 + </div> 130 + </div> 131 + <div class="box warningmessage" data-bind="visible: duplicates().length > 0"> 132 + <p>The migration have detected some duplicated documents, that are probably the consequences of a failed attempt to run the migrator.<br /> 133 + <p>If it's the first time you run the migrator, you might have a problem.</p> 134 + <p>Theses documents are:</p> 135 + <ul data-bind="foreach: {data: duplicates(), as: 'doc'}"> 136 + <li data-bind="text: doc"></li> 137 + </ul> 138 + <p>If you are ok with it, just run the migrator and these documents will be overwritten.</p> 139 + </div> 140 + <div class="box errormessage" data-bind="visible: tooLongs().length > 0"> 141 + <p>We have detected some pages that will have too long path after the migration (limit is #getLocalReferenceMaxLength()). You should rename them (or rename one of their parents) before computing a new plan.</p> 142 + <p>Theses pages are:</p> 143 + <ul data-bind="foreach: {data: tooLongs(), as: 'action'}"> 144 + <li class="monospace"><a data-bind="attr: {href: action.getSourceLink()}" target="_blank"><span data-bind="text: action.serializedSourceDocument()"></span></a> -> <span data-bind="text: action.serializedTargetDocument()"></span></li> 145 + </ul> 146 + </div> 147 + <ul data-bind="if: actions() && !isComputing() && !isPlanEmpty()" id="planTree" class="jstree jstree-xwiki jstree-xwiki-responsive jstree-container-ul"> 148 + <!-- ko template: {name: 'MigrationActionTemplate', data: {'actions': actions() }} --> 149 + <!-- /ko --> 150 + </ul> 151 + <!-- ko if: !isComputing() && isPlanEmpty() --> 152 + <div class="box infomessage"> 153 + <p>There is nothing to do!</p> 154 + </div> 155 + <!-- /ko --> 156 + <!-- ko template: {name: 'DisplayLogs', data: ${escapetool.d}root} --> 157 + <!-- /ko --> 158 + </div> 159 +</Script> 160 +<!------------------------------------------ 161 + Display configuration 162 + -------------------------------------------> 163 +<h2>Configuration</h2> 164 +<form class="xform"> 165 + <div class="row"> 166 + <div class="col-xs-12 col-md-6"> 167 + <dl> 168 + <!-- Excluded pages --> 169 + <dt><label for="excludedPages">Excluded pages</label></dt> 170 + <dd> 171 + <p class="xHint">Page references separated by commas (',')</p> 172 + <p><input type="text" id="excludedPages" data-bind="value: configuration.excludedPages"></p> 173 + </dd> 174 + <!-- Excluded spaces --> 175 + <dt><label for="excludedSpaces">Excluded spaces</label></dt> 176 + <dd> 177 + <p class="xHint">Space references separated by commas (',')</p> 178 + <p><input type="text" id="excludedSpaces" data-bind="value: configuration.excludedSpaces"> </p> 179 + </dd> 180 + <!-- Included spaces --> 181 + <dt><label for="includedSpaces">Included spaces</label></dt> 182 + <dd> 183 + <p class="xHint">Space references separated by commas (',')</p> 184 + <p><input type="text" id="includedSpaces" data-bind="value: configuration.includedSpaces"> </p> 185 + </dd> 186 + </dl> 187 + </div> 188 + <div class="col-xs-12 col-md-6"> 189 + <p><button class="btn btn-default" type="button" data-toggle="collapse" data-target="#advancedSettings" aria-expanded="false" aria-controls="advancedSettings">Advanced Settings</button></p> 190 + <dl id="advancedSettings" class="collapse well"> 191 + <!-- Exclude hidden pages --> 192 + <dt><input type="checkbox" id="excludeHiddenPages" data-bind="checked: configuration.excludeHiddenPages"> <label for="excludeHiddenPages">Exclude hidden pages</label></dt> 193 + <dd><span class="xHint">Most of the hidden pages are techinal content. Moving them can break applications.</span></dd> 194 + <!-- Exclude class pages --> 195 + <dt><input type="checkbox" id="excludeClassPages" data-bind="checked: configuration.excludeClassPages"> <label for="excludeClassPages">Exclude pages having a class</label></dt> 196 + <dd><span class="xHint">The pages are technical and moving them can break applications.</span></dd> 197 + <!-- Don't move children --> 198 + <dt><input type="checkbox" id="dontMoveChildren" data-bind="checked: configuration.dontMoveChildren"> <label for="dontMoveChildren">Do not move children</label></dt> 199 + <dd><span class="xHint">Only convert terminal pages to nested pages, without moving them under their parent.</span></dd> 200 + <!-- Add redirection --> 201 + <dt><input type="checkbox" id="addRedirection" data-bind="checked: configuration.addRedirection"> <label for="addRedirection">Add redirection</label></dt> 202 + <dd><span class="xHint">Add a redirection in the old location.</span></dd> 203 + <!-- Convert preferences --> 204 + <dt><input type="checkbox" id="convertPreferences" data-bind="checked: configuration.convertPreferences"> <label for="convertPreferences">Convert preferences</label></dt> 205 + <dd><span class="xHint">Make sure that the preferences applied on the page remain the same after the move, by dupplicating the preferences on the target document.</span></dd> 206 + <!-- Convert rights --> 207 + <dt><input type="checkbox" id="convertRights" data-bind="checked: configuration.convertRights"> <label for="convertRights">Convert rights (experimental)</label></dt> 208 + <dd><span class="xHint">Make sure that the rights applied on the page remain the same after the move <span class="text-danger">(Currently bugged)</span>.</span></dd> 209 + <!-- Excluded Object Classes --> 210 + <dt><label for="excludedObjectClasses" data-bind="click: toggleXClassList">Exclude classes</label></dt> 211 + <dd> 212 + <p class="xHint" data-bind="click: toggleXClassList">Exclude pages holding an object of one of the specified classes (separated by a coma ',').</p> 213 + <p><textarea id="excludedObjectClasses" data-bind="textInput: configuration.excludedObjectClasses, click: showXClassList" data-localReferenceMaxLength="#getLocalReferenceMaxLength()" data-xclasses="#foreach($class in $xwiki.classList)#if($foreach.count>1),#end${class}#end"></textarea></p> 214 + <div data-bind="visible: xclassListVisible"> 215 + <a data-bind="click: hideXClassList" href="#">$services.icon.renderHTML('remove') Hide</a> 216 + <ul data-bind="foreach: {data: xclasses, as: 'xclass'}" style="list-style-type: none; padding: 0;"> 217 + <li><label><input type="checkbox" data-bind="checked: xclass.selected"/> <span data-bind="text: xclass.name"</span></label></li> 218 + </ul> 219 + </div> 220 + </dd> 221 + </dl> 222 + </div> 223 + </div> 224 + <div class="clearfix"> 225 + <h2>Actions</h2> 226 + <button class="btn btn-success" data-bind="click: startBreakageDetection, disable: isComputing() || isPlanExecuting()">Detect breakages</button> 227 + <button class="btn btn-primary" data-bind="click: computePlan, disable: isComputing() || isPlanExecuting()">Compute plan</button> 228 + <button class="btn btn-primary" data-bind="disable: actions().length == 0 || isPlanExecuting() || tooLongs().length > 0, click: executePlan">Execute plan</button> 229 + <button class="btn btn-default" data-bind="disable: actions().length == 0 || isPlanExecuting(), click: cleanPlan">Clean plan (to free the memory)</button> 230 + </div> 231 +</form> 232 +<!------------------------------------------ 233 + Display plan 234 + -------------------------------------------> 235 +<div data-bind="if: isPlanRequested() && !isPlanExecuting()"> 236 + ## We escape the dollar of the knockout variable '$root' because $root also exists in velocity 237 + <!-- ko template: {name: 'DisplayPlan', data: ${escapetool.d}root} --> 238 + <!-- /ko --> 239 +</div> 240 +<!------------------------------------------ 241 + Execute Plan 242 + -------------------------------------------> 243 +<!-- ko if: isPlanExecuting() && !success()--> 244 +<div class="box" id="planExecuting"> 245 + <p>The plan is being executed and it could take some time. Please wait...</p> 246 + <div class="ui-progress-background"> 247 + <div class="ui-progress-bar green" data-bind="style: {width: progress() + '%'}"></div> 248 + </div> 249 + <!-- ko template: {name: 'DisplayLogs', data: ${escapetool.d}root} --> 250 + <!-- /ko --> 251 +</div> 252 +<!-- /ko --> 253 +<!------------------------------------------ 254 + Display breakages 255 + -------------------------------------------> 256 +<!-- ko if: isBreakageListRequested--> 257 +<h2>Breakages</h2> 258 +<div class="box"> 259 + <div data-bind="if: isComputing"> 260 + <p>The list of broken pages is being computed, please wait...</p> 261 + <div class="ui-progress-background"> 262 + <div class="ui-progress-bar green" data-bind="style: {width: progress() + '%'}"></div> 263 + </div> 264 + <!-- ko template: {name: 'DisplayLogs', data: ${escapetool.d}root} --> 265 + <!-- /ko --> 266 + </div> 267 + <div data-bind="ifnot: isComputing"> 268 + <p>If you don't migrate your pages, <strong data-bind="text: breakageList().size()"></strong> documents will lose their current parent.</p> 269 + <ul data-bind="foreach: breakageList"> 270 + <li>Page <span data-bind="text: document" class="monospace box infomessage" ></span> will lose its current parent <span data-bind="text: actualParent" class="monospace box infomessage"></span> because its location parent is <span data-bind="text: locationParent" class="monospace box infomessage"></span>.</li> 271 + </ul> 272 + </div> 273 +</div> 274 +<!-- /ko --> 275 +<!------------------------------------------ 276 + End message 277 + -------------------------------------------> 278 +<!-- ko if: success() --> 279 + <div class="box successmessage" id="planExecuted"> 280 + The plan have been executed! 281 + </div> 282 + <!-- ko template: {name: 'DisplayLogs', data: ${escapetool.d}root} --> 283 + <!-- /ko --> 284 +<!-- /ko --> 285 +{{/html}} 286 +#end 287 +{{/velocity}} 288 +
- XWiki.DocumentSheetBinding[0]
-
- Sheet
-
... ... @@ -1,1 +1,0 @@ 1 -NestedPagesMigration.Code.WebHomeSheet
- XWiki.JavaScriptExtension[1]
-
- Caching policy
-
... ... @@ -1,0 +1,1 @@ 1 +long - Code
-
... ... @@ -1,0 +1,9 @@ 1 +require.config({ 2 + paths: { 3 + #if ("$!request.minify" == false) 4 + 'knockout': "$services.webjars.url('knockout', 'knockout.debug.js')" 5 + #else 6 + 'knockout': "$services.webjars.url('knockout', 'knockout.js')" 7 + #end 8 + } 9 +}); - Name
-
... ... @@ -1,0 +1,1 @@ 1 +Live view configuration - Parse content
-
... ... @@ -1,0 +1,1 @@ 1 +Yes - Use this extension
-
... ... @@ -1,0 +1,1 @@ 1 +currentPage
- XWiki.JavaScriptExtension[2]
-
- Caching policy
-
... ... @@ -1,0 +1,1 @@ 1 +long - Code
-
... ... @@ -1,0 +1,643 @@ 1 +require(['jquery', 'xwiki-meta', 'knockout'], function ($, xm, ko) { 2 + 'use strict'; 3 + 4 + function localSerializer(document) { 5 + var documentReference = XWiki.Model.resolve(document, XWiki.EntityType.DOCUMENT).relativeTo(xm.documentReference.extractReference(XWiki.EntityType.WIKI)); 6 + return XWiki.Model.serialize(documentReference); 7 + } 8 + 9 + function resolveLocally(document) { 10 + return XWiki.Model.resolve(document, XWiki.EntityType.DOCUMENT).relativeTo(xm.documentReference.extractReference(XWiki.EntityType.WIKI)); 11 + } 12 + 13 + /** 14 + * Class representing a migration action 15 + */ 16 + function MigrationAction(source, target, parent) { 17 + var self = this; 18 + 19 + self.parent = parent; 20 + self.sourceDocument = resolveLocally(source); 21 + self.targetDocument = resolveLocally(target); 22 + self.children = ko.observableArray(); 23 + self.displayChildren = ko.observable(false); 24 + self.enabled = ko.observable(true); 25 + self.preferences = []; 26 + self.rights = []; 27 + self.deletePrevious = false; 28 + self.localReferenceMaxLength = $('#excludedObjectClasses').data().localreferencemaxlength; 29 + 30 + self.serializedSourceDocument = function () { 31 + return XWiki.Model.serialize(self.sourceDocument); 32 + }; 33 + 34 + self.serializedTargetDocument = function () { 35 + return XWiki.Model.serialize(self.targetDocument); 36 + }; 37 + 38 + self.getNumberOfChildren = function () { 39 + var number = self.children().length; 40 + for (var i = 0; i < self.children().length; ++i) { 41 + number += self.children()[i].getNumberOfChildren(); 42 + } 43 + return number; 44 + }; 45 + 46 + self.getTargetName = function () { 47 + return self.targetDocument.getName() == 'WebHome' ? self.targetDocument.parent.getName() : self.targetDocument.getName(); 48 + } 49 + 50 + self.isTooLong = function () { 51 + return self.serializedTargetDocument().length > self.localReferenceMaxLength; 52 + } 53 + 54 + self.getSourceLink = function () { 55 + return new XWiki.Document(self.sourceDocument).getURL(); 56 + } 57 + 58 + self.toggleDisplayChildren = function() { 59 + self.displayChildren(!self.displayChildren()); 60 + } 61 + 62 + self.disableChildren = function () { 63 + for (var i = 0; i < self.children().length; ++i) { 64 + self.children()[i].enabled(false); 65 + } 66 + for (var i = 0; i < self.preferences.length; ++i) { 67 + self.preferences[i].enabled(false); 68 + } 69 + for (var i = 0; i < self.rights.length; ++i) { 70 + self.rights[i].enabled(false); 71 + } 72 + }; 73 + 74 + self.enabled.subscribe(function (newValue) { 75 + if (!newValue) { 76 + self.disableChildren(); 77 + } 78 + }); 79 + 80 + self.enableWithChildren = function() { 81 + self.enabled(true); 82 + for (var i = 0; i < self.preferences.length; ++i) { 83 + self.preferences[i].enabled(true); 84 + } 85 + for (var i = 0; i < self.rights.length; ++i) { 86 + self.rights[i].enabled(true); 87 + } 88 + for (var i = 0; i < self.children().length; ++i) { 89 + self.children()[i].enableWithChildren(); 90 + } 91 + } 92 + 93 + self.getNumberOfPreferences = function () { 94 + return self.preferences.length; 95 + } 96 + 97 + self.getNumberOfRights = function () { 98 + return self.rights.length; 99 + } 100 + } 101 + 102 + /** 103 + * Class representing a preference. 104 + */ 105 + function Preference(property, value, origin) { 106 + var self = this; 107 + 108 + self.property = property; 109 + self.value = value; 110 + self.origin = origin; 111 + self.enabled = ko.observable(true); 112 + 113 + self.getSerializedOrigin = function () { 114 + return localSerializer(self.origin); 115 + }; 116 + 117 + self.getOriginLink = function () { 118 + return new XWiki.Document(resolveLocally(self.origin)).getURL('admin'); 119 + }; 120 + } 121 + 122 + /** 123 + * Class representing a right. 124 + */ 125 + function Right(user, group, level, allow, origin) { 126 + var self = this; 127 + 128 + self.user = user; 129 + self.group = group; 130 + self.level = level; 131 + self.allow = allow; 132 + self.origin = origin; 133 + self.enabled = ko.observable(true); 134 + 135 + self.getType = function () { 136 + return self.user ? 'user' : 'group'; 137 + }; 138 + 139 + self.getTarget = function () { 140 + return self.user ? self.user : self.group; 141 + }; 142 + 143 + self.getAllow = function () { 144 + return self.allow ? 'allow' : 'deny'; 145 + }; 146 + 147 + self.toString = function () { 148 + return self.getType() + ' : ' + self.getTarget() + ', ' + self.level + ' : ' + self.getAllow(); 149 + }; 150 + 151 + self.getSerializedOrigin = function () { 152 + return localSerializer(self.origin); 153 + }; 154 + 155 + self.getOriginLink = function () { 156 + var ref = resolveLocally(self.origin); 157 + return new XWiki.Document(ref).getURL('admin', ref.name == 'WebPreferences' ? 'section=PageAndChildrenRights' : 'section=Rights'); 158 + }; 159 + } 160 + 161 + /** 162 + * Represent a breakage between location parent and actual parent 163 + */ 164 + function Breakage(document, locationParent, actualParent) { 165 + var self = this; 166 + self.document = document; 167 + self.locationParent = locationParent; 168 + self.actualParent = actualParent; 169 + } 170 + 171 + /** 172 + * Class holding the configuration used to compute the plan. 173 + */ 174 + function AppConfiguration() { 175 + this.excludeHiddenPages = ko.observable(true); 176 + this.excludeClassPages = ko.observable(true); 177 + this.dontMoveChildren = ko.observable(false); 178 + this.addRedirection = ko.observable(true); 179 + this.convertPreferences = ko.observable(true); 180 + this.convertRights = ko.observable(false); 181 + this.excludedPages = ko.observable(''); 182 + this.excludedSpaces = ko.observable('XWiki,Admin,NestedPagesMigration'); 183 + this.includedSpaces = ko.observable(''); 184 + this.excludedObjectClasses = ko.observable('XWiki.XWikiUsers,XWiki.XWikiSkins,Panels.PanelClass,Blog.BlogClass,Blog.BlogPostClass,Blog.CategoryClass,ColorThemes.ColorThemeClass,FlamingoThemesCode.ThemeClass,IconThemesCode.IconThemeClass,XWiki.SchedulerJobClass,Menu.MenuClass,XWiki.RedirectClass'); 185 + this.excludedObjectClasses.extend({ notify: 'always' }); 186 + } 187 + 188 + function getExcludedClassesArray(model) { 189 + return model.configuration.excludedObjectClasses().split(','); 190 + } 191 + 192 + function inExcludedClassesArray(name, model) { 193 + return $.inArray(name, getExcludedClassesArray(model)) >= 0; 194 + } 195 + 196 + function appendToString(string, toAppend) { 197 + var result = string; 198 + if (result.length > 0) { 199 + result += ','; 200 + } 201 + result += toAppend; 202 + return result; 203 + } 204 + 205 + function computeNewExcludedClassesList(name, value, model) { 206 + var newList = ''; 207 + var oldList = getExcludedClassesArray(model); 208 + // We walk through the old list to respect the order written in it to avoid a WTF effect 209 + for (var i = 0; i < oldList.length; ++i) { 210 + if (oldList[i] != name) { 211 + newList = appendToString(newList, oldList[i]); 212 + } 213 + } 214 + if (value) { 215 + newList = appendToString(newList, name); 216 + } 217 + model.configuration.excludedObjectClasses(newList); 218 + } 219 + 220 + function initXClassCheckbox(name, model) { 221 + var selected = ko.computed({ 222 + read: function () { 223 + return inExcludedClassesArray(name, model); 224 + }, 225 + write: function (value) { 226 + computeNewExcludedClassesList(name, value, model); 227 + } 228 + }); 229 + return {'name': name, 'selected': selected}; 230 + } 231 + 232 + /** 233 + * Represents a log entry. 234 + */ 235 + function Log(message, level, stackTrace) { 236 + var self = this; 237 + 238 + self.message = message; 239 + self.level = level; 240 + self.stackTrace = stackTrace; 241 + 242 + self.getClass = function () { 243 + return 'log-item-' + self.level.toLowerCase(); 244 + } 245 + } 246 + 247 + /** 248 + * The model of the application. All data and functions used by the application view are stored here. 249 + */ 250 + function AppViewModel() { 251 + var self = this; 252 + 253 + // Fields 254 + self.configuration = new AppConfiguration(); 255 + self.actions = ko.observableArray(); 256 + self.isPlanRequested = ko.observable(false); 257 + self.isBreakageListRequested = ko.observable(false); 258 + self.isComputing = ko.observable(false); 259 + self.xclasses = ko.observableArray(); 260 + self.xclassListVisible = ko.observable(false); 261 + self.jobId = false; 262 + self.progress = ko.observable(0); 263 + self.logs = ko.observableArray(); 264 + self.isPlanExecuting = ko.observable(false); 265 + self.success = ko.observable(false); 266 + self.duplicates = ko.observableArray(); 267 + self.tooLongs = ko.observableArray(); 268 + self.breakageList = ko.observableArray(); 269 + 270 + // Do not refresh logs and actions too often (to get better performances, because a lot of actions and logs 271 + // are pushed in the same time, so it is better to no refresh the UI at every push). 272 + self.logs.extend({ rateLimit: 200}); 273 + self.actions.extend({ rateLimit: 200}); 274 + 275 + self.toggleXClassList = function () { 276 + self.xclassListVisible(!self.xclassListVisible()); 277 + } 278 + 279 + self.showXClassList = function () { 280 + self.xclassListVisible(true); 281 + } 282 + 283 + self.hideXClassList = function () { 284 + self.xclassListVisible(false); 285 + } 286 + 287 + self.getExcludedClassesArray = function () { 288 + return self.configuration.excludedObjectClasses().split(','); 289 + } 290 + 291 + self.inExcludedClassesArray = function() { 292 + return $.inArray(name, self.getExcludedClassesArray()) >= 0; 293 + } 294 + 295 + self.computeNewExcludedClassesList = function (name, value) { 296 + var newList = ''; 297 + var oldList = self.getExcludedClassesArray(); 298 + // We walk through the old list to respect the order written in it to avoid a WTF effect 299 + for (var i = 0; i < oldList.length; ++i) { 300 + if (oldList[i] != name) { 301 + if (newList.length > 0) { 302 + newList += ','; 303 + } 304 + newList += oldList[i]; 305 + } 306 + } 307 + if (value) { 308 + if (newList.length > 0) { 309 + newList += ','; 310 + } 311 + newList += name;; 312 + } 313 + self.configuration.excludedObjectClasses(newList); 314 + } 315 + 316 + self.initXClassCheckbox = function (name) { 317 + var selected = ko.computed({ 318 + read: function () { 319 + return inExcludedClassesArray(name, self); 320 + }, 321 + write: function (value) { 322 + computeNewExcludedClassesList(name, value, self); 323 + } 324 + }); 325 + return {'name': name, 'selected': selected}; 326 + } 327 + 328 + /** 329 + * Initialize the XClasses fields. 330 + */ 331 + self.initXClasses = function() { 332 + var xclasses = $('#excludedObjectClasses').attr('data-xclasses').split(','); 333 + for (var i = 0; i < xclasses.length; ++i) { 334 + self.xclasses.push(initXClassCheckbox(xclasses[i], self)); 335 + } 336 + } 337 + 338 + /** 339 + * Computed observable variable that returns if the plan is empty. 340 + */ 341 + self.isPlanEmpty = ko.computed(function () { 342 + return self.actions().length == 0; 343 + }); 344 + 345 + /** 346 + * Send an ajax request to start a new job for the creation of a plan or the breakage detection 347 + */ 348 + self.startComputationJob = function (action, callback) { 349 + self.progress(0); 350 + self.isComputing(true); 351 + self.actions.removeAll(); 352 + self.duplicates.removeAll(); 353 + self.tooLongs.removeAll(); 354 + self.breakageList.removeAll(); 355 + $.getJSON(new XWiki.Document('Service', 'NestedPagesMigration').getURL('get', 'outputSyntax=plain'), { 356 + 'action' : action, 357 + 'excludeHiddenPages' : self.configuration.excludeHiddenPages(), 358 + 'excludeClassPages' : self.configuration.excludeClassPages(), 359 + 'dontMoveChildren' : self.configuration.dontMoveChildren(), 360 + 'addRedirection' : self.configuration.addRedirection(), 361 + 'convertPreferences' : self.configuration.convertPreferences(), 362 + 'convertRights' : self.configuration.convertRights(), 363 + 'excludedPages' : self.configuration.excludedPages(), 364 + 'excludedSpaces' : self.configuration.excludedSpaces(), 365 + 'includedSpaces' : self.configuration.includedSpaces(), 366 + 'excludedObjectClasses': self.configuration.excludedObjectClasses() 367 + }) 368 + .done(callback) 369 + .fail(function () { 370 + console.log(action == 'startBreakageDetection' ? 'ERROR: Failed to start the breakage detection.' 371 + : 'ERROR: Failed to start a new plan computation.' ); 372 + }); 373 + } 374 + 375 + /** 376 + * Send an ajax request to start a new job for the creation of a plan. 377 + */ 378 + self.computePlan = function() { 379 + self.isPlanRequested(true); 380 + self.isBreakageListRequested(false); 381 + self.startComputationJob('createPlan', function (data) { 382 + self.jobId = data.jobId; 383 + self.logs.removeAll(); 384 + self.getJobStatusAndLogs('createmigrationplan', function() { self.getMigrationPlan(); }); 385 + }); 386 + }; 387 + 388 + /** 389 + * Perform an AJAX request to get the current job status and its logs, so we can update the progress bar and the 390 + * logs UI. 391 + */ 392 + self.getJobStatusAndLogs = function (jobAction, successCallback) { 393 + $.getJSON(new XWiki.Document('Service', 'NestedPagesMigration').getURL('get', 'outputSyntax=plain'), { 394 + 'action' : 'printStatusAndLogs', 395 + 'jobAction' : jobAction 396 + }).done(function (data) { 397 + var logs = data.logs; 398 + for (var i = self.logs().length; i < logs.length; ++i) { 399 + self.logs.push(new Log(logs[i].message, logs[i].level, logs[i].stackTrace)); 400 + } 401 + var state = data.state; 402 + if (state == 'FINISHED') { 403 + self.progress(100); 404 + if (successCallback) { 405 + successCallback(); 406 + } 407 + } else if (state == 'RUNNING' || state == 'NONE') { 408 + self.progress(data.progress * 100); 409 + // retry in 0.8 seconds 410 + setTimeout(function() { self.getJobStatusAndLogs(jobAction, successCallback); }, 800); 411 + } 412 + }); 413 + }; 414 + 415 + /** 416 + * Get the migration plan that have been computed, in order to display it. 417 + */ 418 + self.getMigrationPlan = function () { 419 + $.getJSON(new XWiki.Document('Service', 'NestedPagesMigration').getURL('get', 'outputSyntax=plain'), { 420 + 'action': 'printPlan' 421 + }).done(function (data) { 422 + console.log('INFO: Plan computed'); 423 + var parseAction = function (data, parent) { 424 + var action = new MigrationAction(data.sourceDocument, data.targetDocument, parent); 425 + if (data.children) { 426 + for (var i = 0; i < data.children.length; ++i) { 427 + action.children.push(parseAction(data.children[i], action)); 428 + } 429 + } 430 + if (data.preferences) { 431 + for (var i = 0; i < data.preferences.length; ++i) { 432 + action.preferences[action.preferences.length] = new Preference(data.preferences[i].name, data.preferences[i].value, data.preferences[i].origin); 433 + } 434 + } 435 + if (data.rights) { 436 + for (var i = 0; i < data.rights.length; ++i) { 437 + action.rights[action.rights.length] = new Right(data.rights[i].user, data.rights[i].group, data.rights[i].level, data.rights[i].allow == "true", data.rights[i].origin) 438 + } 439 + } 440 + if (data.deletePrevious) { 441 + action.deletePrevious = true; 442 + self.duplicates.push(action.serializedTargetDocument()); 443 + } 444 + if (action.isTooLong()) { 445 + self.tooLongs.push(action); 446 + } 447 + return action; 448 + }; 449 + 450 + if (data) { 451 + for (var i = 0; i < data.length; ++i) { 452 + self.actions.push(parseAction(data[i], false)); 453 + } 454 + } 455 + // Plan is loaded 456 + self.isComputing(false); 457 + console.log('INFO: Plan have been parsed.'); 458 + }).fail(function () { 459 + new XWiki.widgets.Notification('Failed to load the computed plan', 'error'); 460 + //TODO: being able to restart the computation 461 + }); 462 + }; 463 + 464 + self.startBreakageDetection = function () { 465 + self.isPlanRequested(false); 466 + self.isBreakageListRequested(true); 467 + self.startComputationJob('startBreakageDetection', function (data) { 468 + self.jobId = data.jobId; 469 + self.logs.removeAll(); 470 + self.getJobStatusAndLogs('breakagedetection', function() { self.getBreakages(); }); 471 + }); 472 + }; 473 + 474 + /** 475 + * Get breakage list that have been computed, in order to display it. 476 + */ 477 + self.getBreakages = function () { 478 + $.getJSON(new XWiki.Document('Service', 'NestedPagesMigration').getURL('get', 'outputSyntax=plain'), { 479 + 'action': 'printBreakages' 480 + }).done(function (data) { 481 + for (var i = 0; i < data.length; ++i) { 482 + self.breakageList.push(new Breakage(data[i].documentReference, data[i].locationParent, data[i].actualParent)); 483 + } 484 + // Plan is loaded 485 + self.isComputing(false); 486 + }).fail(function () { 487 + new XWiki.widgets.Notification('Failed to load the breakages', 'error'); 488 + //TODO: being able to restart the computation 489 + }); 490 + }; 491 + 492 + /** 493 + * Called when the user click on the "exclude page" button. 494 + */ 495 + self.excludePage = function() { 496 + var page = this.serializedSourceDocument(); 497 + if (confirm('Are you sure to exclude the page ['+page+'] from the migration? The plan may be recomputed.')) { 498 + self.configuration.excludedPages(appendToString(self.configuration.excludedPages(), page)); 499 + // Adding an exclusion can seriously change the plan (if children are moved), so we re-compute it 500 + if (!self.configuration.dontMoveChildren()) { 501 + self.computePlan(); 502 + } else { 503 + var sourceDoc = this.sourceDocument; 504 + var detectAction = function (action) { 505 + return action.sourceDocument.equals(sourceDoc); 506 + }; 507 + if (this.parent) { 508 + this.parent.children.remove(detectAction); 509 + } else { 510 + self.actions.remove(detectAction); 511 + } 512 + } 513 + } 514 + }; 515 + 516 + /** 517 + * Called when the user click on the "exclude space" button. 518 + */ 519 + self.excludeSpace = function() { 520 + var space = XWiki.Model.serialize(this.sourceDocument.extractReference(XWiki.EntityType.SPACE)); 521 + if (confirm('Are you sure to exclude the space ['+space+'] from the migration? The plan will be recomputed.')) { 522 + self.configuration.excludedSpaces(appendToString(self.configuration.excludedSpaces(), space)); 523 + self.computePlan(); 524 + } 525 + }; 526 + 527 + /** 528 + * Called when the user click on the "set parent" button. 529 + */ 530 + self.setParent = function () { 531 + // The reference needs to be complete in order to use XWiki.Document#getRestURL() 532 + if (this.sourceDocument.getRoot().type != XWiki.EntityType.WIKI) { 533 + this.sourceDocument.appendParent(xm.documentReference.extractReference(XWiki.EntityType.WIKI)) 534 + }; 535 + // First get the current parent 536 + var restURL = new XWiki.Document(this.sourceDocument).getRestURL('', 'media=json'); 537 + var notification = new XWiki.widgets.Notification('Getting information', 'inprogress'); 538 + $.getJSON(restURL).done(function (data) { 539 + notification.hide(); 540 + // Now ask the new parent to set 541 + var parent = prompt("Enter the fullName of the parent that you want to set: (this will be applied immediatly)", data.parent); 542 + if (parent != null) { 543 + notification = new XWiki.widgets.Notification('Saving...', 'inprogress'); 544 + // Set the new parent using the REST API 545 + $.ajax(restURL, { 546 + dataType: 'json', 547 + data: {'parent': parent}, 548 + method: 'PUT' 549 + }).done(function(data) { 550 + // TODO: put something here, and handle error; 551 + self.computePlan(); 552 + notification.replace(new XWiki.widgets.Notification('New parent was set, computing the new plan.', 'done')); 553 + }).fail(function() { 554 + notification.replace(new XWiki.widgets.Notification('Failed to save the page.', 'error')); 555 + }); 556 + } 557 + }).fail(function() { 558 + notification.replace(new XWiki.widgets.Notification('Failed to get the current parent of the page which may not exist.', 'error')); 559 + }); 560 + }; 561 + 562 + /** 563 + * Called when the user clicks on "execute plan" 564 + */ 565 + self.executePlan = function () { 566 + if (!confirm('Are you sure? This operation cannot be undone.')) { 567 + return; 568 + } 569 + self.isPlanExecuting(true); 570 + self.progress(0); 571 + 572 + var getDisabledActions = function (action) { 573 + var disabledActions = ''; 574 + if (!action.enabled()) { 575 + disabledActions += action.serializedSourceDocument() + '_page,'; 576 + } 577 + for (var i = 0; i < action.preferences.length; ++i) { 578 + var preference = action.preferences[i]; 579 + if (!preference.enabled()) { 580 + disabledActions += action.serializedSourceDocument() + '_preference_' + i + ','; 581 + } 582 + } 583 + for (var i = 0; i < action.rights.length; ++i) { 584 + var right = action.rights[i]; 585 + if (!right.enabled()) { 586 + disabledActions += action.serializedSourceDocument() + '_right_' + i + ','; 587 + } 588 + } 589 + for (var i = 0; i < action.children().length; ++i) { 590 + disabledActions += getDisabledActions(action.children()[i]); 591 + } 592 + return disabledActions; 593 + }; 594 + 595 + var disabledActions = ''; 596 + for (var i = 0; i < self.actions().length; ++i) { 597 + var action = self.actions()[i]; 598 + disabledActions += getDisabledActions(action); 599 + } 600 + 601 + $.ajax(new XWiki.Document('Service', 'NestedPagesMigration').getURL('get', 'outputSyntax=plain'), { 602 + 'data': { 603 + 'action' : 'executePlan', 604 + 'addRedirection' : self.configuration.addRedirection(), 605 + 'disabledActions' : disabledActions 606 + }, 607 + 'method': 'POST', 608 + 'data-type': 'json' 609 + }).done(function (data) { 610 + self.jobId = data.jobId; 611 + self.logs.removeAll(); 612 + self.getJobStatusAndLogs('executemigrationplan', function() { self.success(true); }); 613 + }).fail(function () { 614 + console.log('ERROR: Failed to execute the plan.'); 615 + }); 616 + } 617 + 618 + /** 619 + * Clean the plan to free the memory on the server. 620 + */ 621 + self.cleanPlan = function() { 622 + $.ajax(new XWiki.Document('Service', 'NestedPagesMigration').getURL('get', 'outputSyntax=plain'), { 623 + 'data': { 624 + 'action': 'cleanPlan'}, 625 + 'method': 'POST' 626 + }).done(function() { 627 + self.actions.removeAll(); 628 + self.duplicates.removeAll(); 629 + self.tooLongs.removeAll(); 630 + self.isPlanRequested(false); 631 + self.logs.removeAll(); 632 + }); 633 + }; 634 + 635 + // Initialize the XClasses field. 636 + self.initXClasses(); 637 + 638 + }; 639 + 640 + // Activates knockout.js 641 + ko.applyBindings(new AppViewModel()); 642 +}); 643 + - Name
-
... ... @@ -1,0 +1,1 @@ 1 +Live view - Parse content
-
... ... @@ -1,0 +1,1 @@ 1 +No - Use this extension
-
... ... @@ -1,0 +1,1 @@ 1 +currentPage
- XWiki.JavaScriptExtension[3]
-
- Caching policy
-
... ... @@ -1,0 +1,1 @@ 1 +long - Code
-
... ... @@ -1,0 +1,6 @@ 1 +require(['jquery'], function ($) { 2 + $(document).ready(function() { 3 + $(".edit_section").remove(); 4 + }); 5 +}); 6 + - Use this extension
-
... ... @@ -1,0 +1,1 @@ 1 +currentPage
- XWiki.StyleSheetExtension[0]
-
- Caching policy
-
... ... @@ -1,0 +1,1 @@ 1 +long - Code
-
... ... @@ -1,0 +1,15 @@ 1 +#template('colorThemeInit.vm') 2 + 3 +.migration-plan .documentName { 4 + cursor: pointer; 5 +} 6 + 7 +.log { 8 + background-color: $theme.pageContentBackgroundColor; 9 +} 10 + 11 +.log-title { 12 + text-transform: uppercase; 13 + font-size: 0.9em; 14 + font-weight: bold; 15 +} - Content Type
-
... ... @@ -1,0 +1,1 @@ 1 +CSS - Name
-
... ... @@ -1,0 +1,1 @@ 1 +CSS - Parse content
-
... ... @@ -1,0 +1,1 @@ 1 +Yes - Use this extension
-
... ... @@ -1,0 +1,1 @@ 1 +currentPage