Wiki source code of Creating a Tree View

Last modified by Ecaterina Moraru (Valica) on 2017/09/06

Show last authors
1 {{template name="diff_macros.vm"/}}
2
3 {{velocity output="false"}}
4 #macro (unifiedDocContentDiff $alice $bob)
5 #set ($previous = $xwiki.getDocument($alice).content)
6 #set ($next = $xwiki.getDocument($bob).content)
7 {{html}}
8 #unifiedDiff($previous $next)
9 {{/html}}
10 #end
11 {{/velocity}}
12
13 {{box cssClass="floatinginfobox" title="**Contents**"}}
14 {{toc depth="1"/}}
15 {{/box}}
16
17 = Use case =
18
19 Display the employee hierarchy within a company using a tree widget. Each employee has an associated XWiki user. In order to describe the hierarchy we have added two new properties to the ##XWiki.XWikiUsers## xclass:
20
21 * manager:String (identifies the manager of an user)
22 * jobtitle:String (the position the user has inside the company)
23
24 For our use case the XWiki users are synchronized with LDAP so the user profile document has an object of type ##XWiki.LDAPProfileClass##. The relation between an employee and its manager is defined by:
25
26 {{code language="none"}}
27 XWikiUsers.manager(Alice) == LDAPProfileClass.dn(Bob) => Bob is manager of Alice
28 {{/code}}
29
30 = Static Wiki Syntax Tree =
31
32 The most easiest way to create a tree is by using the [[Tree Macro>>extensions:Extension.Tree Macro]] with wiki syntax.
33
34 {{code language="none"}}
35 {{tree}}
36 * [[Ludovic Dubost (CEO)>>XWiki.ldubost]]
37 ** [[Vincent Massol (CTO)>>XWiki.vmassol]]
38 *** [[image:[email protected]||width="24px"]] [[Marius Florea (R&D Engineer)>>XWiki.mflorea]]
39 ** [[Silvia Rusu (Support & QA Manager)>>XWiki.srusu]]
40 *** [[Oana Tăbăranu (Support & Documentation Team Leader)>>XWiki.otabaranu]]
41 ** [[Guillaume Lerouge (Sales & Marketing Director)>>XWiki.glerouge]]
42 {{/tree}}
43 {{/code}}
44
45 The syntax is concise and the tree will degrade nicely when JavaScript is disabled, but unfortunately the links and the custom icons don't work. In the future, the tree could, as an improvement, modify the HTML produced by the wiki syntax to match the [[jsTree>>http://www.jstree.com/]] (the tree widget used under the hood) expectations, but it's not the case right now.
46
47 = Static HTML Tree =
48
49 We can fix the links and the node icons by using HTML:
50
51 {{code language="none"}}
52 {{tree}}
53 {{velocity}}
54 {{html}}
55 <ul>
56 <li data-jstree='{"icon": "$xwiki.getDocument('XWiki.ldubost').getAttachmentURL('ludovic-dubost-2015v13-profile.jpg', 'download', 'width=24')"}'>
57 <a href="$xwiki.getURL('XWiki.ldubost')">Ludovic Dubost (CEO)</a>
58 <ul>
59 <li data-jstree='{"icon": "$xwiki.getDocument('XWiki.vmassol').getAttachmentURL('vincent.png', 'download', 'width=24')"}'>
60 <a href="$xwiki.getURL('XWiki.vmassol')">Vincent Massol (CTO)</a>
61 <ul>
62 <li data-jstree='{"icon": "$xwiki.getDocument('XWiki.mflorea').getAttachmentURL('mflorea.jpg', 'download', 'width=24')"}'>
63 <a href="$xwiki.getURL('XWiki.mflorea')">Marius Florea (R&D Engineer)</a>
64 </li>
65 </ul>
66 </li>
67 <li>
68 <a href="$xwiki.getURL('XWiki.srusu')">Silvia Rusu (Support & QA Manager)</a>
69 <ul>
70 <li>
71 <a href="$xwiki.getURL('XWiki.otabaranu')">Oana Tăbăranu (Support & Documentation Team Leader)</a>
72 </li>
73 </ul>
74 </li>
75 <li>
76 <a href="$xwiki.getURL('XWiki.glerouge')">Guillaume Lerouge (Sales & Marketing Director)</a>
77 </li>
78 </ul>
79 </li>
80 </ul>
81 {{/html}}
82 {{/velocity}}
83 {{/tree}}
84 {{/code}}
85
86 As you can see the syntax is more verbose and we also need to use a bit of:
87
88 * Velocity, in order to compute the link/icon URLs
89 * JSON, in order to specify the custom node icon
90
91 The tree still degrades nicely when JavaScript is disabled but the syntax mix is not appealing. See the [[HTML source documentation>>http://www.jstree.com/docs/html/]] for more details.
92
93 = Static JSON Tree =
94
95 If you want to describe the tree structure in a more semantic way then you better use a JSON source.
96
97 {{code language="none"}}
98 {{tree reference="StaticJSONTreeSource" openTo="mflorea" /}}
99 {{/code}}
100
101 The JSON source looks like this:
102
103 {{code language="none"}}
104 {{velocity wiki="false"}}
105 $response.setContentType('application/json')
106 $jsontool.serialize({
107 'id': 'ldubost',
108 'text': 'Ludovic Dubost (CEO)',
109 'icon': $xwiki.getDocument('XWiki.ldubost').getAttachmentURL('ludovic-dubost-2015v13-profile.jpg', 'download', 'width=24'),
110 'a_attr': {
111 'href': $xwiki.getURL('XWiki.ldubost')
112 },
113 'children': [
114 {
115 'id': 'vmassol',
116 'text': 'Vincent Massol (CTO)',
117 'icon': $xwiki.getDocument('XWiki.vmassol').getAttachmentURL('vincent.png', 'download', 'width=24'),
118 'a_attr': {
119 'href': $xwiki.getURL('XWiki.vmassol')
120 },
121 'children': [
122 {
123 'id': 'mflorea',
124 'text': 'Marius Florea (R&D Engineer)',
125 'icon': $xwiki.getDocument('XWiki.mflorea').getAttachmentURL('mflorea.jpg', 'download', 'width=24'),
126 'a_attr': {
127 'href': $xwiki.getURL('XWiki.mflorea')
128 }
129 }
130 ]
131 },
132 {
133 'id': 'srusu',
134 'text': 'Silvia Rusu (Support & QA Manager)',
135 'a_attr': {
136 'href': $xwiki.getURL('XWiki.srusu')
137 },
138 'children': [
139 {
140 'id': 'otabaranu',
141 'text': 'Oana Tăbăranu (Support & Documentation Team Leader)',
142 'a_attr': {
143 'href': $xwiki.getURL('XWiki.otabaranu')
144 }
145 }
146 ]
147 },
148 {
149 'id': 'glerouge',
150 'text': 'Guillaume Lerouge (Sales & Marketing Director)',
151 'a_attr': {
152 'href': $xwiki.getURL('XWiki.glerouge')
153 }
154 }
155 ]
156 })
157 {{/velocity}}
158 {{/code}}
159
160 Check the [[JSON data documentation>>http://www.jstree.com/docs/json/]] for more details. We still need a bit of Velocity to compute the URLs and to set the content type to ##application/json##. The source is defined in a different document this time. In the future we may add the ability to put JSON directly in the content of the Tree Macro (with a content type parameter) but for now you need a separate document.
161
162 One benefit of using the JSON source is that you can use parameters such as ##openTo## (because we specify node ids in the JSON).
163
164 Note that the tree is no longer degrading nicely when JavaScript is disabled because it needs to make an AJAX request to retrieve the JSON. Moreover, the tree won't scale if it's big. The solution is to implement lazy loading.
165
166 = Dynamic Team Hierarchy Tree v1 (lazy loading) =
167
168 The key to implement lazy loading is the "children" property: instead of specifying the child nodes explicitly (in place) we can set it to:
169
170 * false: meaning the node doesn't have child nodes
171 * true: meaning the node has child nodes but the tree needs to make a separate request to get those child nodes
172
173 {{code language="none"}}
174 {{tree reference="TeamHierarchyTreeSourceV1" /}}
175 {{/code}}
176
177 {{code language="none"}}
178 {{velocity output="false"}}
179 #macro (handleTeamHierarchyTreeRequest)
180 #if ($request.data == 'children')
181 #getChildren($request.id $data)
182 $response.setContentType('application/json')
183 $jsontool.serialize($data)
184 #end
185 #end
186
187 #macro (getChildren $nodeId $return)
188 #if ($nodeId == '#')
189 ## Get the root nodes.
190 #set ($userReference = $NULL)
191 #else
192 ## Get the child nodes of the specified parent node.
193 #set ($userReference = $services.model.createDocumentReference('', 'XWiki', $nodeId))
194 #end
195 #getChildrenQuery($userReference $childrenQuery)
196 #set ($children = [])
197 #foreach ($userId in $childrenQuery.execute())
198 #set ($userReference = $services.model.resolveDocument($userId))
199 #addUserNode($userReference $children)
200 #end
201 #set ($return = $NULL)
202 #setVariable("$return" $children)
203 #end
204
205 #macro (getChildrenQuery $userReference $return)
206 #set ($dn = '')
207 #if ($userReference)
208 #set ($userDocument = $xwiki.getDocument($userReference))
209 #set ($dn = $userDocument.getValue('dn'))
210 #end
211 #set ($query = $services.query.xwql("where doc.object(XWiki.XWikiUsers).manager = :manager"))
212 #set ($query = $query.bindValue('manager', $dn))
213 #set ($return = $NULL)
214 #setVariable("$return" $query)
215 #end
216
217 #macro (addUserNode $userReference $siblings)
218 #set ($userDocument = $xwiki.getDocument($userReference))
219 #set ($jobTitle = $userDocument.getValue('jobtitle'))
220 #set ($userName = $xwiki.getPlainUserName($userReference))
221 #getUserAvatarURL($userReference $avatarURL 24)
222 #getChildrenQuery($userReference $countQuery)
223 #set ($hasChildren = $countQuery.count() > 0)
224 #set ($discard = $siblings.add({
225 'id': $userReference.name,
226 'text': "$userName ($jobTitle)",
227 'icon': $avatarURL.url,
228 'children': $hasChildren,
229 'a_attr': {
230 'href': $xwiki.getURL($userReference)
231 }
232 }))
233 #end
234 {{/velocity}}
235
236 {{velocity wiki="false"}}
237 #if ($xcontext.action == 'get')
238 #handleTeamHierarchyTreeRequest
239 #end
240 {{/velocity}}
241 {{/code}}
242
243 As you can see the tree sends an AJAX request with ##?data=children&id=<parentNodeId>##:
244
245 * when the tree is loading (id=#, because by convention # is the identifier of the tree root; in other words # is the parent of the top level nodes; the # node is not visible)
246 * when a tree node is expanded for the fist time (id=expandedNodeId)
247
248 All we have to do is to write some Velocity code to get the child nodes of the specified parent node.
249
250 Let's see if the ##openTo## parameter still works:
251
252 {{code language="none"}}
253 {{tree reference="TeamHierarchyTreeSourceV1" openTo="mflorea" /}}
254 {{/code}}
255
256 It doesn't.. which is normal because now the tree is not fully loaded until you expand all the nodes. If we want to open the tree to a node that hasn't been added to the tree yet then we need to specify the node path somehow so that the tree can expand all the ancestors of that node.
257
258 = Dynamic Team Hierarchy Tree v2 (open to) =
259
260 {{code language="none"}}
261 {{tree reference="TeamHierarchyTreeSourceV2" openTo="mflorea" /}}
262 {{/code}}
263
264 When the tree doesn't find a specified node it sends a new AJAX request to retrieve the path of that node so that it can load the ancestors nodes before the node itself.
265
266 {{velocity}}
267 #unifiedDocContentDiff('TeamHierarchyTreeSourceV1' 'TeamHierarchyTreeSourceV2')
268 {{/velocity}}
269
270 = Dynamic Team Hierarchy Tree v3 (finder) =
271
272 Next step is to implement a finder for our tree. The idea is to display a text input above the tree that provides suggestions as you type. When a suggestion is selected the tree is opened up to the associated node.
273
274 {{code language="none"}}
275 {{tree reference="TeamHierarchyTreeSourceV2" finder="true" /}}
276 {{/code}}
277
278 For this we need to handle the ##?data=suggestions## request sent by the tree when the user types in the finder text input.
279
280 {{velocity}}
281 #unifiedDocContentDiff('TeamHierarchyTreeSourceV2' 'TeamHierarchyTreeSourceV3')
282 {{/velocity}}
283
284 As you can see we're using [[Solr>>extensions:Extension.Solr Search Application]] to retrieve the suggestions.
285
286 = Dynamic Team Hierarchy Tree v4 (context menu) =
287
288 The tree looks good but we cannot perform any action on the tree nodes. Let's start by adding a context menu that will expose some of the actions.
289
290 {{code language="none"}}
291 {{tree reference="TeamHierarchyTreeSourceV3" finder="true" contextMenu="true" /}}
292 {{/code}}
293
294 This time we need to handle the ##?data=contextMenu## request.
295
296 {{velocity}}
297 #unifiedDocContentDiff('TeamHierarchyTreeSourceV3' 'TeamHierarchyTreeSourceV4')
298 {{/velocity}}
299
300 We made 3 important changes:
301
302 1. We started handling the ##?action=*## requests, although for the moment we just return "The specified action is not supported."
303 1. We're handling the ##?data=contextMenu## request. As you can see the context menu is described using JSON.
304 1. We added more data to the node JSON. This is needed because the tree supports actions only on the nodes that have an associated entity. Note that the additional node data can be used to restrict some actions based on the access rights of the current user.
305
306 = Dynamic Team Hierarchy Tree v5 (drag & drop) =
307
308 The final step is to implement the actions. We'll show how to implement move using drag & drop.
309
310 {{code language="none"}}
311 {{tree reference="TeamHierarchyTreeSourceV3" finder="true" contextMenu="true" dragAndDrop="true" /}}
312 {{/code}}
313
314 {{velocity}}
315 #unifiedDocContentDiff('TeamHierarchyTreeSourceV4' 'TeamHierarchyTreeSourceV5')
316 {{/velocity}}
317
318 As you can see we need to handle the ##?action=move## request. Note that we had to add more meta data to the node JSON. When performing drag & drop the tree needs to know whether the new parent is allowed to have the new child node. We use the ##type## and ##validChildren## properties for this. You can also restrict the move action based on the access rights of the current use, by using the ##canMove## property.

Get Connected