Skip to content
5 changes: 5 additions & 0 deletions zeppelin-web/src/app/home/home.controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,4 +71,9 @@ angular.module('zeppelinWebApp').controller('HomeCtrl', function($scope, noteboo
websocketMsgSrv.reloadAllNotesFromRepo();
$scope.isReloadingNotes = true;
};

$scope.toggleFolderNode = function(node) {
node.hidden = !node.hidden;
};

});
65 changes: 65 additions & 0 deletions zeppelin-web/src/app/home/home.css
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,70 @@ a.navbar-brand:hover {
font: normal normal normal 14px/1 FontAwesome;
}

.dropdown-submenu {
position: relative;
}

.dropdown-submenu a {
display: block;
padding: 3px 20px;
clear: both;
font-weight: normal;
line-height: 1.42857143;
color: #333;
white-space: nowrap;
text-decoration: none;
}

.dropdown-submenu a:hover {
background-color: #f5f5f5;
}

.dropdown-submenu > .dropdown-menu {
top:0;
left:100%;
margin-top:-6px;
margin-left:-1px;
-webkit-border-radius:0 6px 6px 6px;
-moz-border-radius:0 6px 6px 6px;
border-radius:0 6px 6px 6px;
}
.dropdown-submenu:hover > .dropdown-menu {
display:block;
}
/* overwrite the style of the first element of dropdown-menu */
.dropdown-submenu:hover > .dropdown-menu > li:first-child > a:hover {
color: #262626;
text-decoration: none;
background: #f5f5f5;
}
.dropdown-submenu > a:after {
display:block;
content:" ";
float:right;
width:0;
height:0;
border-color:transparent;
border-style:solid;
border-width:5px 0 5px 5px;
border-left-color:#cccccc;
margin-top:5px;
margin-right:-10px;
}
.dropdown-submenu:hover > a:after {
border-left-color:#ffffff;
}
.dropdown-submenu.pull-left {
float:none;
}
.dropdown-submenu.pull-left > .dropdown-menu {
left:-100%;
margin-left:10px;
-webkit-border-radius:6px 0 6px 6px;
-moz-border-radius:6px 0 6px 6px;
border-radius:6px 0 6px 6px;
}

@media (max-width: 767px) {
.navbar-inverse .navbar-nav .open .dropdown-menu > li > a {
color: #D3D3D3;
Expand Down Expand Up @@ -199,6 +263,7 @@ a.navbar-brand:hover {
#notebook-list {
position: relative;
overflow: hidden;
display: inline;
}

@media (min-width: 768px) {
Expand Down
31 changes: 27 additions & 4 deletions zeppelin-web/src/app/home/home.html
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,24 @@
limitations under the License.
-->

<script type="text/ng-template" id="notebook_folder_renderer.html">
<div ng-if="node.children == null">
<a style="text-decoration: none;" href="#/notebook/{{node.id}}">
<i style="font-size: 10px;" class="icon-doc"/> {{node.name || 'Note ' + node.id}}
</a>
</div>
<div ng-if="node.children != null">
<a style="text-decoration: none; cursor: pointer;" ng-click="toggleFolderNode(node)">
<i style="font-size: 10px;" ng-class="node.hidden ? 'icon-folder' : 'icon-folder-alt'" /> {{node.name}}
</a>
<div ng-if="!node.hidden">
<ul style="list-style-type: none; padding-left:15px;">
<li ng-repeat="node in node.children" ng-include="'notebook_folder_renderer.html'" />
</ul>
</div>
</div>
</script>

<div ng-controller="HomeCtrl as home">
<div ng-show="home.staticHome" class="box width-full home">
<div class="zeppelin">
Expand Down Expand Up @@ -41,10 +59,15 @@ <h5><a href="" data-toggle="modal" data-target="#noteNameModal" style="text-deco
<i style="font-size: 15px;" class="icon-notebook"></i> Create new note</a></h5>
<ul id="notebook-names">
<li class="filter-names" ng-include="'components/filterNoteNames/filter-note-names.html'"></li>
<li ng-repeat="note in home.notes.list | filter:query | orderBy:home.arrayOrderingSrv.notebookListOrdering track by $index">
<i style="font-size: 10px;" class="icon-doc"></i>
<a style="text-decoration: none;" href="#/notebook/{{note.id}}">{{note.name || 'Note ' + note.id}}</a>
</li>
<div ng-if="!query || query.name === ''">
<li ng-repeat="node in home.notes.root.children" ng-include="'notebook_folder_renderer.html'" />
</div>
<div ng-if="query && query.name !== ''">
<li ng-repeat="note in home.notes.flatList | filter:query | orderBy:home.arrayOrderingSrv.notebookListOrdering track by $index">
<i style="font-size: 10px;" class="icon-doc"></i>
<a style="text-decoration: none;" href="#/notebook/{{note.id}}">{{note.name || 'Note ' + note.id}}</a>
</li>
</div>
</ul>
</div>
</div>
Expand Down
19 changes: 14 additions & 5 deletions zeppelin-web/src/components/navbar/navbar.html
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,19 @@
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
-->
<script type="text/ng-template" id="notebook_list_renderer.html">
<a ng-if="note.id" href="#/notebook/{{note.id}}">{{note.name || 'Note ' + note.id}} </a>
<li ng-if="!note.id"
class="dropdown-submenu">
<a tabindex="-1" href="javascript: void(0)">{{note.name}}</a>
<ul class="dropdown-menu">
<li ng-repeat="note in note.children track by $index" ng-class="{'active' : navbar.isActive(note.id)}" ng-include="'notebook_list_renderer.html'">
</li>
</ul>
</li>
</script>

<div class="navbar navbar-inverse navbar-fixed-top" style="display: none;" role="navigation" ng-class="{'displayNavBar': !asIframe}">
<div class="container">
<div class="navbar-header">
Expand All @@ -32,10 +44,7 @@
<li><a href="" data-toggle="modal" data-target="#noteNameModal"><i class="fa fa-plus"></i> Create new note</a></li>
<li class="divider"></li>
<div id="notebook-list" class="scrollbar-container">
<li class="filter-names" ng-include="'components/filterNoteNames/filter-note-names.html'"></li>
<li ng-repeat="note in navbar.notes.list | filter:query | orderBy:navbar.arrayOrderingSrv.notebookListOrdering track by $index"
ng-class="{'active' : navbar.isActive(note.id)}">
<a href="#/notebook/{{note.id}}">{{note.name || 'Note ' + note.id}} </a>
<li ng-repeat="note in navbar.notes.root.children track by $index" ng-class="{'active' : navbar.isActive(note.id)}" ng-include="'notebook_list_renderer.html'">
</li>
</div>
</ul>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ <h4 class="modal-title" ng-show="notenamectrl.clone">Clone note</h4>
placeholder="Note name" type="text" class="form-control"
id="noteName" ng-model="note.notename" ng-enter="notenamectrl.handleNameEnter()">
</div>
Use '/' to create folders. Example: /NoteDirA/Notebook1
</div>
<div class="modal-footer">
<button type="button" id="createNoteButton"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,52 @@
'use strict';

angular.module('zeppelinWebApp').factory('notebookListDataFactory', function() {
var notes = {};

notes.list = [];
var notes = {
root: {children: []},
flatList: [],

notes.setNotes = function(notesList) {
notes.list = angular.copy(notesList);
setNotes: function(notesList) {
// a flat list to boost searching
notes.flatList = angular.copy(notesList);

// construct the folder-based tree
notes.root = {children: []};
_.reduce(notesList, function(root, note) {
var noteName = note.name || note.id;
var nodes = noteName.match(/([^\\\][^\/]|\\\/)+/g);

// recursively add nodes
addNode(root, nodes, note.id);

return root;
}, notes.root);
}
};

var addNode = function(curDir, nodes, noteId) {
if (nodes.length === 1) { // the leaf
curDir.children.push({
name : nodes[0],
id : noteId
});
} else { // a folder node
var node = nodes.shift();
var dir = _.find(curDir.children,
function(c) {return c.name === node && c.children !== undefined;});
if (dir !== undefined) { // found an existing dir
addNode(dir, nodes, noteId);
} else {
var newDir = {
name : node,
hidden : true,
children : []
};
curDir.children.push(newDir);
addNode(newDir, nodes, noteId);
}
}
};

return notes;
});
});
69 changes: 69 additions & 0 deletions zeppelin-web/test/spec/factory/notebookList.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
'use strict';

describe('Factory: NotebookList', function() {

var notebookList;

beforeEach(function () {
module('zeppelinWebApp');

inject(function ($injector) {
notebookList = $injector.get('notebookListDataFactory');
});
});

it('should generate both flat list and folder-based list properly', function() {
var notesList = [
{name: 'A', id: '000001'},
{name: 'B', id: '000002'},
{id: '000003'}, // notebook without name
{name: '/C/CA', id: '000004'},
{name: '/C/CB', id: '000005'},
{name: '/C/CB/CBA', id: '000006'}, // same name with a dir
{name: '/C/CB/CBA', id: '000007'}, // same name with another note
{name: 'C///CB//CBB', id: '000008'}
];
notebookList.setNotes(notesList);

var flatList = notebookList.flatList;
expect(flatList.length).toBe(8);
expect(flatList[0].name).toBe('A');
expect(flatList[0].id).toBe('000001');
expect(flatList[1].name).toBe('B');
expect(flatList[2].name).toBeUndefined();
expect(flatList[3].name).toBe('/C/CA');
expect(flatList[4].name).toBe('/C/CB');
expect(flatList[5].name).toBe('/C/CB/CBA');
expect(flatList[6].name).toBe('/C/CB/CBA');
expect(flatList[7].name).toBe('C///CB//CBB');

var folderList = notebookList.root.children;
expect(folderList.length).toBe(4);
expect(folderList[0].name).toBe('A');
expect(folderList[0].id).toBe('000001');
expect(folderList[1].name).toBe('B');
expect(folderList[2].name).toBe('000003');
expect(folderList[3].name).toBe('C');
expect(folderList[3].id).toBeUndefined();
expect(folderList[3].children.length).toBe(3);
expect(folderList[3].children[0].name).toBe('CA');
expect(folderList[3].children[0].id).toBe('000004');
expect(folderList[3].children[0].children).toBeUndefined();
expect(folderList[3].children[1].name).toBe('CB');
expect(folderList[3].children[1].id).toBe('000005');
expect(folderList[3].children[1].children).toBeUndefined();
expect(folderList[3].children[2].name).toBe('CB');
expect(folderList[3].children[2].id).toBeUndefined();
expect(folderList[3].children[2].children.length).toBe(3);
expect(folderList[3].children[2].children[0].name).toBe('CBA');
expect(folderList[3].children[2].children[0].id).toBe('000006');
expect(folderList[3].children[2].children[0].children).toBeUndefined();
expect(folderList[3].children[2].children[1].name).toBe('CBA');
expect(folderList[3].children[2].children[1].id).toBe('000007');
expect(folderList[3].children[2].children[1].children).toBeUndefined();
expect(folderList[3].children[2].children[2].name).toBe('CBB');
expect(folderList[3].children[2].children[2].id).toBe('000008');
expect(folderList[3].children[2].children[2].children).toBeUndefined();
});

});