From 6fcfcbfbd72e255dca29baadd32e040202b69e91 Mon Sep 17 00:00:00 2001 From: Carlo Date: Fri, 4 Nov 2016 11:13:02 +0100 Subject: [PATCH] TT#4315 ngcp-csc mocked buddylist/chat further implementation - chat users are now grouped in teams - user can add/remove chat channels and add/remove users with both drag and drop or bulk selection - message textarea now accepts line breaks (enter + shift) - added icons to trigger call/videocalls (will be implemented in separat ticket) Change-Id: Ibc48ff24fbe85c8462ae035970826756d6f56d73 --- app/locales.js | 91 ++++++++++++- app/store/ChatList.js | 20 +-- classic/sass/src/view/pages/chat/Chat.scss | 29 +++- classic/src/view/main/MainController.js | 1 + classic/src/view/pages/chat/Chat.js | 77 +++++------ classic/src/view/pages/chat/ChatController.js | 40 +++++- classic/src/view/pages/chat/ChatList.js | 86 ++++++++++-- .../src/view/pages/chat/ChatListController.js | 124 +++++++++++++----- classic/src/view/pages/chat/ChatListModel.js | 8 +- resources/data/chat.json | 6 +- resources/data/chatlist.json | 79 ++++++++--- 11 files changed, 425 insertions(+), 136 deletions(-) diff --git a/app/locales.js b/app/locales.js index 2af562ab..84089498 100644 --- a/app/locales.js +++ b/app/locales.js @@ -466,11 +466,25 @@ Ext.define('Ngcp.csc.locales', { }, chat: { title: { - en: 'Team', - it: 'Team', - de: 'Team', - fr: 'Team', - sp: 'Team' + en: 'Buddy List', + it: 'Buddy List', + de: 'Buddy List', + fr: 'Buddy List', + sp: 'Buddy List' + }, + new_group: { + en: 'Create new team', + it: 'Create new team', + de: 'Create new team', + fr: 'Create new team', + sp: 'Create new team' + }, + provide_name: { + en: 'Chat group name', + it: 'Chat group name', + de: 'Chat group name', + fr: 'Chat group name', + sp: 'Chat group name' }, msg_box:{ empty_text:{ @@ -487,6 +501,52 @@ Ext.define('Ngcp.csc.locales', { de: 'You can start a private conversation with {0} here.', fr: 'You can start a private conversation with {0} here.', sp: 'You can start a private conversation with {0} here.' + }, + alerts:{ + user_ddrop: { + en: 'Only users can be dropped', + it: 'Only users can be dropped', + de: 'Only users can be dropped', + fr: 'Only users can be dropped', + sp: 'Only users can be dropped' + }, + user_in_group: { + en: 'User already in group.', + it: 'User already in group.', + de: 'User already in group.', + fr: 'User already in group.', + sp: 'User already in group.' + }, + user_added: { + en: '{0} added to {1} channel.', + it: '{0} added to {1} channel.', + de: '{0} added to {1} channel.', + fr: '{0} added to {1} channel.', + sp: '{0} added to {1} channel.' + }, + + choose_valid_name: { + en: 'Please choose a valid name.', + it: 'Please choose a valid name.', + de: 'Please choose a valid name.', + fr: 'Please choose a valid name.', + sp: 'Please choose a valid name.' + }, + channel_created: { + en: 'Channel created', + it: 'Channel created', + de: 'Channel created', + fr: 'Channel created', + sp: 'Channel created' + }, + + channel_delete: { + en: 'Do you really want to delete {0} channel?', + it: 'Do you really want to delete {0} channel?', + de: 'Do you really want to delete {0} channel?', + fr: 'Do you really want to delete {0} channel?', + sp: 'Do you really want to delete {0} channel?' + } } }, common: { @@ -573,6 +633,27 @@ Ext.define('Ngcp.csc.locales', { de: 'submit', fr: 'submit', sp: 'submit' + }, + add: { + en: 'add', + it: 'add', + de: 'add', + fr: 'add', + sp: 'add' + }, + call: { + en: 'Call', + it: 'Call', + de: 'Call', + fr: 'Call', + sp: 'Call' + }, + videocall: { + en: 'Videocall', + it: 'Videocall', + de: 'Videocall', + fr: 'Videocall', + sp: 'Videocall' } } diff --git a/app/store/ChatList.js b/app/store/ChatList.js index d76234b9..303dc78c 100644 --- a/app/store/ChatList.js +++ b/app/store/ChatList.js @@ -1,25 +1,17 @@ Ext.define('NgcpCsc.store.ChatList', { - extend: 'Ext.data.Store', + extend: 'Ext.data.TreeStore', alias: 'store.chatlist', storeId: 'ChatList', - model: 'NgcpCsc.model.ChatList', - - autoLoad: true, - proxy: { type: 'ajax', - url: '/resources/data/chatlist.json', - reader: { - type: 'json', - rootProperty: 'data' - } + url: 'resources/data/chatlist.json' }, - sorters: { - direction: 'DESC', - property: 'online' - } + sorters: [{ + property: 'online', + direction: 'DESC' + }] }); diff --git a/classic/sass/src/view/pages/chat/Chat.scss b/classic/sass/src/view/pages/chat/Chat.scss index 65b31f02..23b36626 100644 --- a/classic/sass/src/view/pages/chat/Chat.scss +++ b/classic/sass/src/view/pages/chat/Chat.scss @@ -201,6 +201,27 @@ } .navigation-email { + .x-grid-row:hover { + .x-phone-display:before { + font-family: FontAwesome; + content: "\f095"; + margin-left: 5px; + } + .x-video-display:before { + font-family: FontAwesome; + content: "\f03d"; + margin-left: 10px; + } + + .x-drop-display{ + position: relative; + right: 6px; + &:before { + font-family: FontAwesome; + content: "\f00d"; + } + } + } // @include box-shadow(0px,2px,8px,0px,rgba(0,0,0,.15)); @include box-shadow(0, 1px, 2px, 0, rgba(0,0,0,0.2)); @@ -278,7 +299,7 @@ } .new-message-cont { - margin-left: 203px; + margin-right: 10px; } .submit-new-message { @@ -293,3 +314,9 @@ .private-conversation-text { padding: 30px; } +.online-user { + color: $online-menu-item-color !important; +} +.offline-user { + color: $offline-menu-item-color !important; +} diff --git a/classic/src/view/main/MainController.js b/classic/src/view/main/MainController.js index 7328a8b2..f7b36d04 100644 --- a/classic/src/view/main/MainController.js +++ b/classic/src/view/main/MainController.js @@ -167,6 +167,7 @@ Ext.define('NgcpCsc.view.main.MainController', { showMessage: function(success, msg) { var msgConsole = this.lookupReference('console'), msgColor = (success) ? 'green-txt' : 'red-txt'; + msgConsole.removeCls(['green-txt' , 'red-txt']); msgConsole.addCls(msgColor); msgConsole.setText(msg); msgConsole.getEl().fadeIn(); diff --git a/classic/src/view/pages/chat/Chat.js b/classic/src/view/pages/chat/Chat.js index cbbd5f55..b72649d4 100644 --- a/classic/src/view/pages/chat/Chat.js +++ b/classic/src/view/pages/chat/Chat.js @@ -7,53 +7,46 @@ Ext.define('NgcpCsc.view.pages.chat.Chat', { controller: 'chat', - layout: 'hbox', + layout: { + type: 'hbox', + align: 'stretch' + }, items: [{ xtype: 'chatlist', - width: 200, - padding: '10 20 20', - height: '100%' + flex:1, + padding: '0 1 0 3', + scrollable:true }, { xtype: 'tabpanel', - width: '90%', - height: '100%', - items: [{ - title: Ngcp.csc.locales.chat.title[localStorage.getItem('languageSelected')], - xtype: 'chat-notifications', - id: 'chat-notifications', - scrollable: true, - bind: { - store: '{notifications}' - } - }] - }], - - dockedItems: [{ - xtype: 'toolbar', - cls: 'new-message-cont', - fixed: true, - padding: '0 0 10 0', - dock: 'bottom', - items: [{ - xtype: 'textarea', - bind: { - value: '{new_message}' - }, - cls: 'new-message', - name: 'new-message', - enableKeyEvents: true, - height: 100, - width: '95%', - listeners: { - keypress: 'onPressEnter' - }, - emptyText: Ngcp.csc.locales.chat.msg_box.empty_text[localStorage.getItem('languageSelected')] - }, { - xtype: 'button', - cls: 'submit-new-message', - text: Ngcp.csc.locales.common.submit[localStorage.getItem('languageSelected')], - handler: 'onPressSubmitBtn' + flex: 5, + plugins: 'tabreorderer', + dockedItems: [{ + xtype: 'toolbar', + cls: 'new-message-cont', + fixed: true, + padding: '0 0 10 0', + dock: 'bottom', + items: [{ + xtype: 'textarea', + bind: { + value: '{new_message}' + }, + cls: 'new-message', + name: 'new-message', + enableKeyEvents: true, + height: 100, + width: '95%', + emptyText: Ngcp.csc.locales.chat.msg_box.empty_text[localStorage.getItem('languageSelected')], + listeners: { + keypress: 'onPressEnter' + } + }, { + xtype: 'button', + cls: 'submit-new-message', + text: Ngcp.csc.locales.common.submit[localStorage.getItem('languageSelected')], + handler: 'onPressSubmitBtn' + }] }] }] diff --git a/classic/src/view/pages/chat/ChatController.js b/classic/src/view/pages/chat/ChatController.js index cbf98ecb..fd6a6a85 100644 --- a/classic/src/view/pages/chat/ChatController.js +++ b/classic/src/view/pages/chat/ChatController.js @@ -6,13 +6,15 @@ Ext.define('NgcpCsc.view.pages.chat.ChatController', { listen: { controller: { '#chatlist': { - openpmtab: 'openPM' + openpmtab: 'openPM', + openchanneltab: 'openChat', + destroytab: 'closeChat' } } }, onPressEnter: function(field, e) { - if (e.getKey() == e.ENTER) { + if (!e.shiftKey && e.getKey() == e.ENTER) { e.preventDefault(); this.submitMessage(); } @@ -24,7 +26,7 @@ Ext.define('NgcpCsc.view.pages.chat.ChatController', { submitMessage: function(msg, user) { var message = msg || this.getViewModel().get('new_message'); - if (message.length < 1) + if (message.length < 1 || !this.getView().down('tabpanel').getActiveTab()) return; var chatStore = this.getView().down('tabpanel').getActiveTab().getStore('notifications'); var lastMsg = chatStore.getAt(chatStore.getCount() - 1) || this.getViewModel().getStore('notifications').findRecord('id', this.getView().down('tabpanel').getActiveTab().name); @@ -40,7 +42,7 @@ Ext.define('NgcpCsc.view.pages.chat.ChatController', { "isActive": true, "time": Ext.String.format("{0}:{1}", hour, minutes), "thumbnail": (user) ? user.get('thumbnail') : "resources/images/user-profile/2.png", - "content": message + "content": message.replace(/(?:\r\n|\r|\n)/g, '
') }); chatStore.add(messageModel); this.clearMsg(); @@ -57,7 +59,7 @@ Ext.define('NgcpCsc.view.pages.chat.ChatController', { }, openPM: function(item, rec) { - var tab = this.getView().down('[name=' + rec.get('id') + ']'); + var tab = this.getView().down('[name=' + rec.get('uid') + ']'); if (rec.get('name') == 'administrator') // hardcoded administrator return; if (!tab) { @@ -69,12 +71,38 @@ Ext.define('NgcpCsc.view.pages.chat.ChatController', { cls: 'private-conversation-text', deferEmptyText: false, emptyText: Ext.String.format(Ngcp.csc.locales.chat.start_conversation[localStorage.getItem('languageSelected')], rec.get('name')), - name: rec.get('id'), + name: rec.get('uid'), store: Ext.create('Ext.data.Store', { model: 'NgcpCsc.model.ChatNotification' }) }); } this.getView().down('tabpanel').setActiveTab(tab); + }, + openChat: function(rec) { + var tab = this.getView().down('[name=' + rec.get('name') + ']'); + if (rec.get('name') == 'Buddies') + return; + if (!tab) { + tab = this.getView().down('tabpanel').add({ + xtype: 'chat-notifications', + title: rec.get('name'), + name: rec.get('name'), + closable: true, + scrollable: true, + bind: { + store: '{notifications}' + } + }); + } + this.getView().down('tabpanel').setActiveTab(tab); + }, + closeChat: function(tabToClose) { + var tabToClose = this.getView().down('[name=' + tabToClose + ']'); + var chatList = this.getView().down('#chatlist'); + if (tabToClose){ + tabToClose.destroy(); + } + chatList.getView().refresh(); } }); diff --git a/classic/src/view/pages/chat/ChatList.js b/classic/src/view/pages/chat/ChatList.js index c1edfcd7..85975a4a 100644 --- a/classic/src/view/pages/chat/ChatList.js +++ b/classic/src/view/pages/chat/ChatList.js @@ -1,23 +1,91 @@ Ext.define('NgcpCsc.view.pages.chat.ChatList', { - extend: 'Ext.menu.Menu', + extend: 'Ext.tree.Panel', alias: 'widget.chatlist', - viewModel: { - type: 'chatlist' - }, - controller: 'chatlist', + itemId:'chatlist', + title: Ngcp.csc.locales.chat.title[localStorage.getItem('languageSelected')], + hideHeaders: true, + cls: 'navigation-email', - iconCls: 'x-fa fa-group', + store: 'ChatList', + + rootVisible: false, - floating: false, + viewConfig: { + plugins: { + ptype: 'treeviewdragdrop', + sortOnDrop: true, + containerScroll: true + }, + listeners: { + beforecellclick: 'preventTabOpen' + } + }, + tools: [{ + type: 'plus', + tooltip: Ngcp.csc.locales.chat.new_group[localStorage.getItem('languageSelected')], + callback: 'showTabBar' + }], + + dockedItems: [{ + xtype: 'toolbar', + dock: 'top', + hidden: true, + items: [{ + xtype: 'textfield', + name: 'newChatName', + width: '80%', + emptyText: Ngcp.csc.locales.chat.provide_name[localStorage.getItem('languageSelected')], + minLength: 1 + }, { + xtype: 'button', + text: Ngcp.csc.locales.common.add[localStorage.getItem('languageSelected')], + handler: 'createNewChannel' + }] + }], listeners: { - click: 'itemListClicked' - } + beforeitemclick: 'nodeClicked', + beforedrop: 'onBeforeUserDropped' + }, + + defaults: { + menuDisabled: true + }, + + columns: [{ + xtype: 'treecolumn', + dataIndex: 'name', + renderer: 'renderStatus', + flex: 2 + }, { + xtype: 'actioncolumn', + text: 'actions', + flex: 1, + items: [{ + tooltip: Ngcp.csc.locales.common.call[localStorage.getItem('languageSelected')], + getClass: function(value, context) { + return (context.record && context.record.get('leaf')) ? 'x-phone-display' : ''; + }, + handler: 'startCall' + }, { + tooltip: Ngcp.csc.locales.common.videocall[localStorage.getItem('languageSelected')], + getClass: function(value, context) { + return (context.record && context.record.get('leaf')) ? 'x-video-display' : ''; + }, + handler: 'startVideoCall' + }, { + //tooltip: Ngcp.csc.locales.common.delete[localStorage.getItem('languageSelected')], + getClass: function(value, context) { + return (context.record && !context.record.get('leaf')) ? 'x-drop-display' : ''; + }, + handler: 'deleteNode' + }] + }] }); diff --git a/classic/src/view/pages/chat/ChatListController.js b/classic/src/view/pages/chat/ChatListController.js index d7410d4e..8d655be1 100644 --- a/classic/src/view/pages/chat/ChatListController.js +++ b/classic/src/view/pages/chat/ChatListController.js @@ -5,47 +5,101 @@ Ext.define('NgcpCsc.view.pages.chat.ChatListController', { id: 'chatlist', // needed as reference in ChatController listeners - init: function() { - var me = this, - friendsStore = me.getViewModel().getStore('friends'); - - //Trigger local sorting once new data is available - friendsStore.on('load', function(store) { - store.sort(); + renderStatus: function(val, meta, rec) { + if (rec.get('leaf')) { + rec.set('iconCls', 'x-fa fa-circle ' + (rec.get('online') ? 'online-user' : 'offline-user')); + } + return val; + }, + onBeforeUserDropped: function(node, data, overModel, dropPosition, dropHandlers) { + var chatListStore = this.getView().getStore('ChatList'); + var dropTeam = overModel.get('leaf') ? overModel.parentNode : overModel; + dropHandlers.cancelDrop(); + switch (true) { + case !data.records[0].get('leaf'): // happens when a group is dropped + this.fireEvent('showmessage', false, Ngcp.csc.locales.chat.alerts.user_ddrop[localStorage.getItem('languageSelected')]); + break; + case !!dropTeam.findChild('uid', data.records[0].get('uid')): // checks if user is already in team + this.fireEvent('showmessage', false, Ngcp.csc.locales.chat.alerts.user_in_group[localStorage.getItem('languageSelected')]); + break; + default: + dropTeam.insertChild(0, data.records[0].copy(null)); + chatListStore.sort('online', 'DESC'); + this.fireEvent('showmessage', true, Ext.String.format(Ngcp.csc.locales.chat.alerts.user_added[localStorage.getItem('languageSelected')], data.records[0].get('name'), dropTeam.get('name'))); + } + }, + showTabBar: function() { + var chatList = this.getView(); + chatList.getDockedItems('toolbar[dock="top"]')[0].setVisible(true); + chatList.getStore().each(function(rec) { + if (rec.get('leaf')) { + rec.set('checked', false); + } }); + }, + createNewChannel: function() { + var chatList = this.getView(); + var tbar = chatList.getDockedItems('toolbar[dock="top"]')[0]; + var newChatName = tbar.down('[name=newChatName]'); + var selectedUsers = chatList.getChecked(); + if (newChatName.getValue().length < 1) { + this.fireEvent('showmessage', false, Ngcp.csc.locales.chat.alerts.choose_valid_name[localStorage.getItem('languageSelected')]); + return; + } - //Sort locally and then update menu - friendsStore.on('sort', function(store) { - me.mutateData(store, store.getRange()); + var newNode = chatList.getRootNode().insertChild(chatList.getStore().getCount(), { + "name": newChatName.getValue(), + "iconCls": "x-fa fa-wechat", + "expanded": true, + "children": [] }); - - me.callParent(arguments); + Ext.each(selectedUsers, function(user) { + newNode.insertChild(0, user.copy(null)); + }); + chatList.getStore().each(function(rec) { + rec.set('checked', null); + }); + chatList.getStore().sort('online', 'DESC'); + this.fireEvent('showmessage', true, Ngcp.csc.locales.chat.alerts.channel_created[localStorage.getItem('languageSelected')]); + tbar.hide(); + newChatName.reset(); }, - - mutateData: function(store, records) { - var view = this.getView(), - arr = [], - len = records.length, - i; - - for (i = 0; i < len; i++) { - arr.push({ - xtype: 'menuitem', - uId: records[i].get('id'), - text: records[i].get('name'), - cls: 'font-icon ' + (records[i].get('online') ? 'online-user' : 'offline-user') - }); + preventTabOpen: function(view, cell, cellIdx, record, row, rowIdx, eOpts) { + if (cellIdx === 1) { // prevents tabs to be opened in case user clicked on icons in actioncolumn (startcall/delete/...) + return false; } - - Ext.suspendLayouts(); - view.removeAll(true); - view.add(arr); - Ext.resumeLayouts(true); }, + startCall: function(grid, rowIndex, colIndex, item, e) { + this.fireEvent('showmessage', true, 'Todo start call'); + }, + startVideoCall: function(grid, rowIndex, colIndex) { + this.fireEvent('showmessage', true, 'Todo start videocall'); + }, + nodeClicked: function(node, record, item, index, e) { + if (record.get('checked') != null) + return; + if (!record.get('leaf')) + this.fireEvent('openchanneltab', record); + else + this.fireEvent('openpmtab', null, record); + return false; - itemListClicked: function(menu, item) { - var selectedUser = Ext.getStore('ChatList').findRecord('id', item.uId, 0, false, true, true); - if (selectedUser && selectedUser.get('online')) - this.fireEvent('openpmtab', null, selectedUser); + }, + deleteNode: function(grid, rowIndex, colIndex) { + var nodeToDelete = grid.getStore().getAt(rowIndex); + var me = this; + if (nodeToDelete.get('leaf')) + return; + Ext.Msg.show({ + message: Ext.String.format(Ngcp.csc.locales.chat.alerts.channel_delete[localStorage.getItem('languageSelected')], nodeToDelete.get('name')), + buttons: Ext.Msg.YESNO, + icon: Ext.Msg.QUESTION, + fn: function(btn) { + if (btn === 'yes') { + nodeToDelete.remove(); + me.fireEvent('destroytab', nodeToDelete.get('name')); + } + } + }); } }); diff --git a/classic/src/view/pages/chat/ChatListModel.js b/classic/src/view/pages/chat/ChatListModel.js index 0c72e5f7..765fd70e 100644 --- a/classic/src/view/pages/chat/ChatListModel.js +++ b/classic/src/view/pages/chat/ChatListModel.js @@ -4,12 +4,8 @@ Ext.define('NgcpCsc.view.pages.chat.ChatListModel', { alias: 'viewmodel.chatlist', stores: { - friends: { - //Store reference - type: 'chatlist', - - //Auto load - autoLoad: true + buddyList: { + type: 'chatlist' } } }); diff --git a/resources/data/chat.json b/resources/data/chat.json index 1f4df66d..e5726787 100644 --- a/resources/data/chat.json +++ b/resources/data/chat.json @@ -1,6 +1,6 @@ { "data": [{ - "id": 840, + "uid": 840, "name": "Jil Sanchez", "date": "10/27/2016", "isActive": true, @@ -8,7 +8,7 @@ "thumbnail": "resources/images/user-profile/9.png", "content": "There are many variations of passages of Lorem Ipsum available, but the majority have suffered alteration in some form, by injected humour, or randomised words which don\'t look even slightly believable. If you are going to use a passage of Lorem Ipsum, you need to be sure there isn\'t anything embarrassing hidden in the middle of text. All the Lorem Ipsum generators on the Internet tend to repeat predefined chunks as necessary, making this the first true generator on the Internet. It uses a dictionary of over 200 Latin words, combined with a handful of model sentence structures, to generate Lorem Ipsum which looks reasonable. The generated Lorem Ipsum is therefore always free from repetition, injected humour, or non-characteristic words etc." }, { - "id": 252, + "uid": 252, "name": "Ben Wright", "date": "10/27/2010", "isActive": true, @@ -16,7 +16,7 @@ "thumbnail": "resources/images/user-profile/10.png", "content": "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry\'s standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum." }, { - "id": 162, + "uid": 162, "name": "Allen Morris", "date": "10/27/2016", "isActive": true, diff --git a/resources/data/chatlist.json b/resources/data/chatlist.json index 98935195..c1df30f5 100644 --- a/resources/data/chatlist.json +++ b/resources/data/chatlist.json @@ -1,27 +1,76 @@ -{ - "data": [{ - "id": 840, +[{ + "name": "Buddies", + "iconCls": "x-fa fa-group", + "expanded": true, + "children": [{ + "uid": 840, "online": true, - "name": "Jil Sanchez" + "name": "Jil Sanchez", + "leaf": true }, { - "id": 1, + "uid": 1, "online": false, - "name": "Oneill Franklin" + "name": "Oneill Franklin", + "leaf": true }, { - "id": 3, + "uid": 3, "online": false, - "name": "Branch Allison" + "name": "Branch Allison", + "leaf": true }, { - "id": 252, + "uid": 252, "online": true, - "name": "Ben Wright" + "name": "Ben Wright", + "leaf": true }, { - "id": 162, + "uid": 162, "online": true, - "name": "Allen Morris" + "name": "Allen Morris", + "leaf": true }, { - "id": 5, + "uid": 5, "online": false, - "name": "Suzette Powell" + "name": "Suzette Powell", + "leaf": true }] -} +}, { + "name": "Development", + "iconCls": "x-fa fa-wechat", + "expanded": true, + "children": [{ + "uid": 162, + "online": true, + "name": "Allen Morris", + "leaf": true + },{ + "uid": 840, + "online": true, + "name": "Jil Sanchez", + "leaf": true + },{ + "uid": 252, + "online": true, + "name": "Ben Wright", + "leaf": true + }, { + "uid": 5, + "online": false, + "name": "Suzette Powell", + "leaf": true + }] +}, { + "name": "Operations", + "iconCls": "x-fa fa-wechat", + "expanded": true, + "children": [{ + "uid": 162, + "online": true, + "name": "Allen Morris", + "leaf": true + }, { + "uid": 5, + "online": false, + "name": "Suzette Powell", + "leaf": true + }] +}]