mockup/master: Closes #772, search for files and text within files.

Oshane Bailey <jenkins-z4DKO/[email protected]>
Newsgroups gmane.comp.web.zope.plone.cvs
Message-ID <[email protected]>
Repository: mockup
Branch: refs/heads/master
Date: 2017-06-11T23:10:19-05:00
Author: Oshane Bailey (b4oshany) <[email protected]>
Commit: https://github.com/plone/mockup/commit/85981066f4b3d808a84e188f075835e14506a85c

Closes #772, search for files and text within files.

Files changed:
A mockup/js/ui/templates/dropdown.xml
A mockup/js/ui/views/anchor.js
A mockup/js/ui/views/dropdown.js
A mockup/patterns/filemanager/js/findfile.js
A mockup/patterns/filemanager/js/findinfiles.js
A mockup/patterns/filemanager/templates/popover.xml
M CHANGES.rst
M mockup/patterns/filemanager/js/basepopover.js
M mockup/patterns/filemanager/pattern.filemanager.less
M mockup/patterns/filemanager/pattern.js
M mockup/patterns/filemanager/templates/app.xml
M mockup/patterns/relateditems/pattern.js
D mockup/patterns/filemanager/js/filesearch.js

diff --git a/CHANGES.rst b/CHANGES.rst
index 5d839f8a..a74c15fe 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -10,12 +10,17 @@ Breaking changes:
 
 New features:
 
- - make thumb size in folder contents listing adjustable/supressable 
+ - Search for files and opening the file upon click
+ - Search for text within files and opening the file upon click
+ - Added Bootstrap Dropdown menu to the UI views
+ - Added Anchor (a) tag to the UI views
+
+ - make thumb size in folder contents listing adjustable/supressable
    replace meaningless paper clip icon (fontello) with mime type icons
    from mimetype registry
    https://github.com/plone/Products.CMFPlone/issues/1734
    [fgrcon]
- 
+
 - For ``pat-modal``, let the ajax modal variant acquire it's ajax url when the modal is shown instead when the pattern is initialized.
   This makes the modal respect a dynamically changed href attribute on a anchor tag, after it was initialized.
   [thet]
diff --git a/mockup/js/ui/templates/dropdown.xml b/mockup/js/ui/templates/dropdown.xml
new file mode 100644
index 00000000..ffb2492a
--- /dev/null
+++ b/mockup/js/ui/templates/dropdown.xml
@@ -0,0 +1,9 @@
+
+<a style="padding-top: 8px" href="#" class="btn btn-default dropdown-toggle" type="button" id="dropdown-menu-" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true">
+  <span class="dropdown-title">
+
+  </span>
+  <span class="caret"></span>
+</a>
+<ul class="dropdown-menu items dropdown-content" aria-labelledby="dropdown-menu-">
+</ul>
diff --git a/mockup/js/ui/views/anchor.js b/mockup/js/ui/views/anchor.js
new file mode 100644
index 00000000..df83e45e
--- /dev/null
+++ b/mockup/js/ui/views/anchor.js
@@ -0,0 +1,56 @@
+define([
+  'underscore',
+  'mockup-ui-url/views/base',
+  'mockup-patterns-tooltip'
+], function(_, BaseView, Tooltip) {
+  'use strict';
+
+  var AnchorView = BaseView.extend({
+    tagName: 'a',
+    className: 'alink',
+    eventPrefix: 'button',
+    context: 'default',
+    idPrefix: 'alink-',
+    attributes: {
+      'href': '#'
+    },
+    extraClasses: [],
+    tooltip: null,
+    template: '<% if (icon) { %><span class="glyphicon glyphicon-<%= icon %>"></span><% } %> <%= title %>',
+    events: {
+      'click': 'handleClick'
+    },
+    initialize: function(options) {
+      if (!options.id) {
+        var title = options.title || '';
+        options.id = title !== '' ? title.toLowerCase().replace(' ', '-') : this.cid;
+      }
+      BaseView.prototype.initialize.apply(this, [options]);
+
+      this.on('render', function() {
+        this.$el.attr('title', this.options.title || '');
+        this.$el.attr('aria-label', this.options.title || this.options.tooltip || '');
+        _.each(this.extraClasses, function(klass){
+          this.$el.addClass(klass);
+        });
+      }, this);
+    },
+    handleClick: function(e) {
+      e.preventDefault();
+      if (!this.$el.prop('disabled')) {
+        this.uiEventTrigger('click', this, e);
+      }
+    },
+    serializedModel: function() {
+      return _.extend({'icon': '', 'title': ''}, this.options);
+    },
+    disable: function() {
+      this.$el.prop('disabled', true);
+    },
+    enable: function() {
+      this.$el.prop('disabled', false);
+    }
+  });
+
+  return AnchorView;
+});
diff --git a/mockup/js/ui/views/dropdown.js b/mockup/js/ui/views/dropdown.js
new file mode 100644
index 00000000..6e4505a0
--- /dev/null
+++ b/mockup/js/ui/views/dropdown.js
@@ -0,0 +1,56 @@
+define([
+  'underscore',
+  'mockup-ui-url/views/buttongroup',
+  'text!mockup-ui-url/templates/dropdown.xml',
+], function(_, ButtonGroup, DropdownTemplate) {
+  'use strict';
+
+  var DropdownView = ButtonGroup.extend({
+    idPrefix: 'btngroup-dropdown-',
+    template: DropdownTemplate,
+    itemContainer: "ul.dropdown-content",
+    title: null,
+
+    initialize: function(options) {
+      ButtonGroup.prototype.initialize.apply(this, [options]);
+
+      this.on('render', function() {
+        this.renderTitle();
+      }, this);
+    },
+
+    renderTitle: function() {
+      var title = this.options.title;
+      if(this.options.title == undefined){
+        title = this.title;
+        if(this.title == null){
+          title = "Menu Option";
+        }
+      }
+      this.$('.dropdown-title').empty().append(title);
+    },
+
+    renderItems: function() {
+      var $container;
+
+      if (this.itemContainer !== null) {
+        $container = $(this.itemContainer, this.$el);
+        if ($container.length === 0) {
+          throw 'Item Container element not found.';
+        }
+      } else {
+        $container = this.$el;
+      }
+
+      var $item = null;
+      _.each(this.items, function(view) {
+        $item = $("<li></li>");
+        $item.append(view.render().$el.removeClass("btn"));
+        $container.append($item);
+      }, this);
+    },
+
+  });
+
+  return DropdownView;
+});
diff --git a/mockup/patterns/filemanager/js/basepopover.js b/mockup/patterns/filemanager/js/basepopover.js
index fee2ab53..61796918 100644
--- a/mockup/patterns/filemanager/js/basepopover.js
+++ b/mockup/patterns/filemanager/js/basepopover.js
@@ -1,22 +1,52 @@
 define([
   'underscore',
-  'mockup-ui-url/views/popover'
-], function(_, PopoverView) {
+  'mockup-ui-url/views/popover',
+  'text!mockup-patterns-filemanager-url/templates/popover.xml',
+], function(_, PopoverView, PopoverTemplate) {
   'use strict';
 
   var FileManagerPopover = PopoverView.extend({
     className: 'popover',
     title: _.template('nothing'),
     content: _.template('<div/>'),
+    template: PopoverTemplate,
     initialize: function(options) {
       this.app = options.app;
       PopoverView.prototype.initialize.apply(this, [options]);
     },
+    afterRender: function () {
+      var self = this;
+      self.$el.find(".popover-close").click(function(e){
+        self.hide(true);
+      });
+      return self;
+    },
+    getBodyClassName: function(){
+      var name = 'popover-';
+      if(this.options.id){
+        name += this.options.id + '-';
+      }
+      name += 'active';
+      return name;
+    },
     render: function() {
       var self = this;
       PopoverView.prototype.render.call(this);
       return self;
     },
+    hide: function(closePopover) {
+      if(this.closeOnOutClick || closePopover == true){
+        this.opened = false;
+        this.$el.removeClass('active');
+        if (this.triggerView) {
+          this.triggerView.$el.removeClass('active');
+          this.triggerView.$el.attr('aria-hidden', 'true');
+        }
+        this.uiEventTrigger('hide', this);
+        this.$el.attr('aria-hidden', 'true');
+        $('body').removeClass(this.getBodyClassName());
+      }
+    },
     toggle: function(button, e) {
       PopoverView.prototype.toggle.apply(this, [button, e]);
       var self = this;
diff --git a/mockup/patterns/filemanager/js/filesearch.js b/mockup/patterns/filemanager/js/filesearch.js
deleted file mode 100644
index be5c35d1..00000000
--- a/mockup/patterns/filemanager/js/filesearch.js
+++ /dev/null
@@ -1,73 +0,0 @@
-define([
-  'jquery',
-  'underscore',
-  'mockup-patterns-filemanager-url/js/basepopover',
-  'translate'
-], function($, _, PopoverView, _t) {
-  'use strict';
-
-  var FileSearchView = PopoverView.extend({
-    className: 'popover filesearch',
-    title: _.template('<%= _t("File Search") %>'),
-    content: _.template(
-      '<form>' +
-        '<div class="input-group">' +
-          '<input type="text" class="search form-control" ' +
-                  'id="file-search-field" placeholder="<%= _t("Find theme resource in plone") %>">' +
-          '<span class="input-group-btn">' +
-            '<input type="submit" class="btn btn-primary" value="<%= _t("Search") %>"/>' +
-          '</span>' +
-        '</div>' +
-      '</form>' +
-      '<ul class="results list-group">' +
-      '</ul>'
-    ),
-    render: function() {
-      var self = this;
-      PopoverView.prototype.render.call(this);
-      self.$form = self.$('form');
-      self.$results = self.$('.results');
-      self.$form.submit(function(e) {
-        e.preventDefault();
-        $.ajax({
-          url: self.app.options.resourceSearchUrl,
-          dataType: 'json',
-          success: function(data) {
-            self.$results.empty();
-            _.each(data, function(item) {
-              var $item = $(
-                '<li class="list-group-item" data-id="' + item.id + '">' +
-                  '<span class="badge"><a href=#">' + _t('Customize') + '</a></span>' +
-                  item.id +
-                '</li>');
-              $('a', $item).click(function(e) {
-                e.preventDefault();
-                self.customize($(this).parents('li').eq(0).attr('data-id'));
-              });
-              self.$results.append($item);
-            });
-          }
-        });
-      });
-      return self;
-    },
-    customize: function(resource) {
-      var self = this;
-      self.app.doAction('customize', {
-        type: 'POST',
-        data: {
-          resource: resource
-        },
-        success: function(data) {
-          self.hide();
-          // clear out
-          self.$('input.search').attr('value', '');
-          self.$results.empty();
-          self.app.refreshTree();
-        }
-      });
-    }
-  });
-
-  return FileSearchView;
-});
diff --git a/mockup/patterns/filemanager/js/findfile.js b/mockup/patterns/filemanager/js/findfile.js
new file mode 100644
index 00000000..d4c0c3dd
--- /dev/null
+++ b/mockup/patterns/filemanager/js/findfile.js
@@ -0,0 +1,101 @@
+define([
+  'jquery',
+  'underscore',
+  'mockup-patterns-filemanager-url/js/basepopover',
+  'translate'
+], function($, _, PopoverView, _t) {
+  'use strict';
+
+  var FindFile = PopoverView.extend({
+    className: 'popover filesearch',
+    closeOnOutClick: false,
+    backdropOptions: {
+      zIndex: '1009',
+      opacity: '0.4',
+      className: 'backdrop backdrop-popover',
+      classActiveName: 'backdrop-active',
+      closeOnEsc: false,
+      closeOnClick: false
+    },
+    title: _.template('<%= _t("Find File") %>'),
+    content: _.template(
+      '<form>' +
+        '<div class="input-group">' +
+          '<input type="text" class="search form-control" ' +
+                  'id="file-search-field" placeholder="<%= _t("Find theme resource in plone") %>">' +
+        '</div>' +
+        '<div class="input-group">' +
+          '<input type="submit" class="btn btn-primary" value="<%= _t("Search") %>"/>' +
+        '</div>' +
+      '</form><br/>' +
+      '<ul class="results list-group">' +
+      '</ul>'
+    ),
+    appendToResults: function(item){
+      var self = this;
+      var $item = $(
+        '<li class="list-group-item">' +
+          '<span class="badge"><a data-target="' + item.path + '" href=#">' +
+          _t(item.filename) + '</a></span>' +
+        '</li>');
+      $('a', $item).click(function(e) {
+        e.preventDefault();
+        self.findfile($(this).attr('data-target'));
+      });
+      self.$results.append($item);
+    },
+    filterFiles: function(patt, data){
+      var self = this;
+      _.each(data, function(item) {
+        if(item.folder){
+          self.filterFiles(patt, item.children);
+        }else{
+          if(patt.test(item.filename)){
+            self.appendToResults(item);
+            self.noMatches++;
+          }
+        }
+      });
+    },
+    render: function() {
+      var self = this;
+      PopoverView.prototype.render.call(this);
+      self.$form = self.$('form');
+      self.$searchFor = self.$("input");
+      self.$results = self.$('.results');
+      self.$form.submit(function(e) {
+        e.preventDefault();
+        $.ajax({
+          url: self.app.options.actionUrl + '?action=dataTree',
+          dataType: 'json',
+          success: function(data) {
+            self.$results.empty();
+            self.noMatches = 0;
+            var searchFor = self.$searchFor.val();
+            var patt = new RegExp(searchFor, "g");
+            self.filterFiles(patt, data);
+            if(self.noMatches == 0){
+              self.$results.append("<span>No results found for " + searchFor + "</span>");
+            }
+          }
+        });
+      });
+      return self;
+    },
+    findfile: function(resource) {
+      var self = this;
+      self.app.doAction('getFile', {
+        data: {
+          path: resource
+        },
+        dataType: 'json',
+        success: function(data) {
+          self.app.fileData[resource] = data;
+          self.app.openEditor(resource);
+        }
+      });
+    }
+  });
+
+  return FindFile;
+});
diff --git a/mockup/patterns/filemanager/js/findinfiles.js b/mockup/patterns/filemanager/js/findinfiles.js
new file mode 100644
index 00000000..904741f9
--- /dev/null
+++ b/mockup/patterns/filemanager/js/findinfiles.js
@@ -0,0 +1,134 @@
+define([
+  'jquery',
+  'underscore',
+  'mockup-patterns-filemanager-url/js/basepopover',
+  'translate'
+], function($, _, PopoverView, _t) {
+  'use strict';
+
+  var FindInFiles = PopoverView.extend({
+    className: 'popover filesearch',
+    closeOnOutClick: false,
+    backdropOptions: {
+      zIndex: '1009',
+      opacity: '0.4',
+      className: 'backdrop backdrop-popover',
+      classActiveName: 'backdrop-active',
+      closeOnEsc: false,
+      closeOnClick: false
+    },
+    title: _.template('<%= _t("File in File") %>'),
+    content: _.template(
+      '<form>' +
+        '<div class="input-group">' +
+          '<input type="text" class="search form-control" ' +
+                  'id="file-search-field" placeholder="<%= _t("Find text witin theme resource in plone") %>">' +
+        '</div>' +
+        '<div class="input-group">' +
+          '<input type="submit" class="btn btn-primary" value="<%= _t("Search") %>"/>' +
+        '</div>' +
+      '</form><br/>' +
+      '<ul style="max-height: 400px; overflow: auto;" class="results list-group">' +
+      '</ul>'
+    ),
+    appendToResults: function(item){
+      var self = this, seen = null;
+      var file_item =
+        '<li class="list-group-item" data-id="' + item.file.label + '">' +
+          '<span class="badge">' + _t(item.file.filename) + '</span><ul>';
+      for(var x in item.lines){
+        seen = item.lines[x];
+        file_item += '<li class="list-group-item" data-id="' + item.file.label + '">' +
+          '<span class="badge"><a class="ff-open-file" data-target="'+item.file.path+'" ' +
+          'target-line="'+seen.line+'" href="#">Line ' +
+          '<span style="display: inline-block; width: 100px;">' + seen.line +
+          '</span><span>'+seen.text+'</span><a></span></li>';
+      }
+      file_item += '</ul></li>';
+      var $item = $(file_item);
+      $('a', $item).click(function(e) {
+        e.preventDefault();
+        self.findinfiles($(this).attr("data-target"));
+      });
+      self.$results.append($item);
+    },
+
+    filterFile: function(patt, item){
+      var self = this;
+      $.ajax({
+        url: self.app.options.actionUrl + '?action=getFile&path='+item.path.replace("/", "%2F"),
+        dataType: 'json',
+        success: function(data) {
+          var contents = data["contents"];
+          if(contents == undefined){
+            return;
+          }
+          var lines = contents.split("\n");
+          var seen = [], line = '';
+          var result = null;
+          for(var x in lines){
+            line = lines[x];
+            result = patt.exec(line);
+            if(result != null){
+              seen.push({
+                "line": parseInt(x) + 1,
+                "text": '<b>'+result[0]+'</b>'+line.substr(result["index"] + result[0].length, 20)
+              });
+            }
+          }
+          if(seen.length > 0){
+            self.appendToResults({file: item, lines: seen});
+            self.noMatches += seen.length;
+          }
+        }
+      });
+    },
+    filterFiles: function(patt, data){
+      var self = this;
+      _.each(data, function(item) {
+        if(item.folder){
+          self.filterFiles(patt, item.children);
+        }else{
+          self.filterFile(patt, item);
+        }
+      });
+    },
+    render: function() {
+      var self = this;
+      PopoverView.prototype.render.call(this);
+      self.$form = self.$('form');
+      self.$searchFor = self.$("input");
+      self.$results = self.$('.results');
+      self.$form.submit(function(e) {
+        e.preventDefault();
+        $.ajax({
+          url: self.app.options.actionUrl + '?action=dataTree',
+          dataType: 'json',
+          success: function(data) {
+            self.$results.empty();
+            self.noMatches = 0;
+            var searchFor = self.$searchFor.val();
+            var patt = new RegExp(searchFor, "g");
+            self.filterFiles(patt, data);
+          }
+        });
+      });
+      return self;
+    },
+    findinfiles: function(resource) {
+      var self = this;
+      self.app.doAction('getFile', {
+        data: {
+          path: resource
+        },
+        dataType: 'json',
+        success: function(data) {
+          self.app.fileData[resource] = data;
+          self.app.openEditor(resource);
+        }
+      });
+    }
+  });
+
+  return FindInFiles;
+});
diff --git a/mockup/patterns/filemanager/pattern.filemanager.less b/mockup/patterns/filemanager/pattern.filemanager.less
index b5fd4216..c37f28ac 100644
--- a/mockup/patterns/filemanager/pattern.filemanager.less
+++ b/mockup/patterns/filemanager/pattern.filemanager.less
@@ -27,6 +27,11 @@
     .popover:extend(.popover all){}
     .popover-title:extend(.popover-title all){}
     .popover-content:extend(.popover-content all){}
+
+    .popover{
+        max-width: 600px;
+    }
+
     .arrow:extend(.arrow all){}
 
     .table:extend(.table all){}
diff --git a/mockup/patterns/filemanager/pattern.js b/mockup/patterns/filemanager/pattern.js
index 5ab7b96a..916780f3 100644
--- a/mockup/patterns/filemanager/pattern.js
+++ b/mockup/patterns/filemanager/pattern.js
@@ -40,19 +40,23 @@ define([
   'mockup-ui-url/views/toolbar',
   'mockup-ui-url/views/button',
   'mockup-ui-url/views/buttongroup',
+  'mockup-ui-url/views/anchor',
+  'mockup-ui-url/views/dropdown',
   'mockup-patterns-filemanager-url/js/addnew',
   'mockup-patterns-filemanager-url/js/newfolder',
-  'mockup-patterns-filemanager-url/js/filesearch',
+  'mockup-patterns-filemanager-url/js/findfile',
+  'mockup-patterns-filemanager-url/js/findinfiles',
   'mockup-patterns-filemanager-url/js/delete',
   'mockup-patterns-filemanager-url/js/customize',
   'mockup-patterns-filemanager-url/js/rename',
   'mockup-patterns-filemanager-url/js/upload',
   'translate',
   'mockup-utils',
-  'text!mockup-ui-url/templates/popover.xml'
+  'text!mockup-ui-url/templates/popover.xml',
+  'text!mockup-ui-url/templates/dropdown.xml'
 ], function($, Base, _, Tree, TextEditor, AppTemplate, Toolbar,
-  ButtonView, ButtonGroup, AddNewView, NewFolderView, DeleteView,
-  FileSearchView,
+  ButtonView, ButtonGroup, AnchorView, DropdownView,
+  AddNewView, NewFolderView, FindFileView, FindInFilesView, DeleteView,
   CustomizeView, RenameView, UploadView, _t, utils) {
   'use strict';
 
@@ -137,16 +141,26 @@ define([
         }),
         app: self
       });
-      var fileSearchView = new FileSearchView({
-        triggerView: new ButtonView({
-          id: 'filesarch',
-          title: _t('File Search'),
+      var findFileView = new FindFileView({
+        triggerView: new AnchorView({
+          id: 'findfile',
+          title: _t('Find File'),
           tooltip: _t('Find theme resource in plone'),
           icon: 'file',
           context: 'default'
         }),
         app: self
       });
+      var findinFilesView = new FindInFilesView({
+        triggerView: new AnchorView({
+          id: 'findinfiles',
+          title: _t('Find in Files'),
+          tooltip: _t('Find text within theme resource in plone'),
+          icon: 'file',
+          context: 'default'
+        }),
+        app: self
+      });
       var renameView = new RenameView({
         triggerView: new ButtonView({
           id: 'rename',
@@ -168,18 +182,29 @@ define([
         app: self
       });
 
+      var find_menu = new DropdownView({
+        title: _t('Find'),
+        items: [
+          findFileView.triggerView,
+          findinFilesView.triggerView
+        ],
+        id: 'find',
+        app: self
+      })
+
       self.views = [
         newFolderView,
         addNewView,
-        fileSearchView,
+        findFileView,
+        findinFilesView,
         renameView,
         deleteView
       ];
       var mainButtons = [
         self.saveBtn,
         newFolderView.triggerView,
-        fileSearchView.triggerView,
         addNewView.triggerView,
+        find_menu,
         renameView.triggerView,
         deleteView.triggerView
       ];
@@ -304,6 +329,7 @@ define([
         }
       });
 
+
       self.$tree.bind('tree.select', function(e) {
         if (e.node === null) {
           self.toggleButtons(false);
diff --git a/mockup/patterns/filemanager/templates/app.xml b/mockup/patterns/filemanager/templates/app.xml
index 11d594ff..e5194500 100644
--- a/mockup/patterns/filemanager/templates/app.xml
+++ b/mockup/patterns/filemanager/templates/app.xml
@@ -1,5 +1,7 @@
 <div id="toolbar">
 </div>
+<div id="toolbar-action">
+</div>
 <div class="container">
     <div class="tree">
     </div>
@@ -14,4 +16,4 @@
                 </div>
             </div>
         </nav>
-</div>
\ No newline at end of file
+</div>
diff --git a/mockup/patterns/filemanager/templates/popover.xml b/mockup/patterns/filemanager/templates/popover.xml
new file mode 100644
index 00000000..a276f070
--- /dev/null
+++ b/mockup/patterns/filemanager/templates/popover.xml
@@ -0,0 +1,12 @@
+<div class="arrow"></div>
+<div class="popover-label">
+<div style="position: relative;" class="popover-title">
+</div>
+<a style="position: absolute; top: 5px; right: 3px; font-size: 16px; color: #999;"
+   href="#" class="popover-close">
+  <span class="glyphicon glyphicon-remove"></span>
+</a>
+</div>
+<div class="items popover-content">
+</div>
+
diff --git a/mockup/patterns/relateditems/pattern.js b/mockup/patterns/relateditems/pattern.js
index f10d3145..00ff3883 100644
--- a/mockup/patterns/relateditems/pattern.js
+++ b/mockup/patterns/relateditems/pattern.js
@@ -98,11 +98,11 @@ define([
   'mockup-utils',
   'pat-registry',
   'translate',
-  'text!mockup-patterns-relateditems-url/templates/breadcrumb.xml',
-  'text!mockup-patterns-relateditems-url/templates/favorite.xml',
-  'text!mockup-patterns-relateditems-url/templates/result.xml',
-  'text!mockup-patterns-relateditems-url/templates/selection.xml',
-  'text!mockup-patterns-relateditems-url/templates/toolbar.xml',
+//  'text!mockup-patterns-relateditems-url/templates/breadcrumb.xml',
+//  'text!mockup-patterns-relateditems-url/templates/favorite.xml',
+//  'text!mockup-patterns-relateditems-url/templates/result.xml',
+//  'text!mockup-patterns-relateditems-url/templates/selection.xml',
+//  'text!mockup-patterns-relateditems-url/templates/toolbar.xml',
   'bootstrap-dropdown'
 ], function($, _, Base, Select2, ButtonView, utils, registry, _t,
             BreadcrumbTemplate,



------------------------------------------------------------------------------
Check out the vibrant tech community on one of the world's most
engaging tech sites, Slashdot.org! http://sdm.link/slashdot
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.