Wiki source code of Creating a Tree View
Last modified by Ecaterina Moraru (Valica) on 2017/09/06
Show last authors
author | version | line-number | content |
---|---|---|---|
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:XWiki.mflorea@mflorea.jpg||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. |