0 Votes

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
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
Change comment: Install extension [org.xwiki.contrib:application-nestedpagesmigrator-ui/0.7.3]

Summary

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> -&gt; <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