diff --git a/CHANGES.rst b/CHANGES.rst index 3745d7150..4e83dbeda 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,7 +1,7 @@ Changelog ========= -2.1.4 (unreleased) +2.2.0 (unreleased) ------------------ Incompatibilities: @@ -13,11 +13,45 @@ New: - set XML syntax coloring for .pt files in text editor [ebrehault] +- Structure now accept customization options for a number of things in + the form of requirejs modules. This currently includes the extended + menuOptions definition, the menuGenerator per result item, the click + handler the link for each individual item, and the collection module + for interaction with the server side API for item generation. + + Where applicable, the default implementation are now named requirejs + includes with those as the defaults to the relevant parameters. + + Incidentally, this also required a major cleanup/refactoring of how + the ResultCollection class interacts with the pattern and its support + classes. + [metatoaster] + +- Structure now supports IPublishTraverse style subpaths for push state. + [metatoaster] + +- Alternative parameter/syntax for specification of the pushState url to + be inline with the usage of ``{path}`` token in URL templates. + [metatoaster] + +- Structure can use the ``viewURL`` from a returned data item, alongside + with the previous default of simply appending ``/view`` to the + ``getURL`` attribute if this was not provided, for its view URL, + [metatoaster] + Fixes: - Fix ``Makefile`` to use ``mockup/build`` instead of ``build``. [thet] +- Fix structure so rendering does not fail when paste button is missing. + [metatoaster] + +- Fix structure so that different views can have its own saved visible + column ordering settings. Also loosen the coupling of the columns to + the data to aid in view rendering. + [metatoaster] + 2.1.3 (2016-02-27) New: diff --git a/mockup/js/utils.js b/mockup/js/utils.js index 90f83d83f..f4ada0d92 100644 --- a/mockup/js/utils.js +++ b/mockup/js/utils.js @@ -266,6 +266,14 @@ define([ .toString(16).substring(1)); }; + var getWindow = function() { + var win = window; + if (win.parent !== window) { + win = win.parent; + } + return win; + }; + return { generateId: generateId, parseBodyTag: function(txt) { @@ -309,6 +317,7 @@ define([ return $el.val(); } }, + getWindow: getWindow, featureSupport: { /* well tested feature support for things we use in mockup. diff --git a/mockup/patterns/structure/js/actionmenu.js b/mockup/patterns/structure/js/actionmenu.js new file mode 100644 index 000000000..8d3b86aa7 --- /dev/null +++ b/mockup/patterns/structure/js/actionmenu.js @@ -0,0 +1,93 @@ +define([ +], function() { + 'use strict'; + + var menuOptions = { + 'cutItem': [ + 'mockup-patterns-structure-url/js/actions', + 'cutClicked', + '#', + 'Cut', + ], + 'copyItem': [ + 'mockup-patterns-structure-url/js/actions', + 'copyClicked', + '#', + 'Copy' + ], + 'pasteItem': [ + 'mockup-patterns-structure-url/js/actions', + 'pasteClicked', + '#', + 'Paste' + ], + 'move-top': [ + 'mockup-patterns-structure-url/js/actions', + 'moveTopClicked', + '#', + 'Move to top of folder' + ], + 'move-bottom': [ + 'mockup-patterns-structure-url/js/actions', + 'moveBottomClicked', + '#', + 'Move to bottom of folder' + ], + 'set-default-page': [ + 'mockup-patterns-structure-url/js/actions', + 'setDefaultPageClicked', + '#', + 'Set as default page' + ], + 'selectAll': [ + 'mockup-patterns-structure-url/js/actions', + 'selectAll', + '#', + 'Select all contained items' + ], + 'openItem': [ + 'mockup-patterns-structure-url/js/navigation', + 'openClicked', + '#', + 'Open' + ], + 'editItem': [ + 'mockup-patterns-structure-url/js/navigation', + 'editClicked', + '#', + 'Edit' + ], + }; + + var ActionMenu = function(menu) { + // If an explicit menu was specified as an option to AppView, this + // constructor will not override that. + if (menu.menuOptions !== null) { + return menu.menuOptions; + } + + var result = {}; + result['cutItem'] = menuOptions['cutItem']; + result['copyItem'] = menuOptions['copyItem']; + if (menu.app.pasteAllowed && menu.model.attributes.is_folderish) { + result['pasteItem'] = menuOptions['pasteItem']; + } + if (!menu.app.inQueryMode() && menu.options.canMove !== false) { + result['move-top'] = menuOptions['move-top']; + result['move-bottom'] = menuOptions['move-bottom']; + } + if (!menu.model.attributes.is_folderish && menu.app.setDefaultPageUrl) { + result['set-default-page'] = menuOptions['set-default-page']; + } + if (menu.model.attributes.is_folderish) { + result['selectAll'] = menuOptions['selectAll']; + } + if (menu.options.header) { + result['openItem'] = menuOptions['openItem']; + } + result['editItem'] = menuOptions['editItem']; + return result; + }; + + return ActionMenu; +}); diff --git a/mockup/patterns/structure/js/actions.js b/mockup/patterns/structure/js/actions.js new file mode 100644 index 000000000..be5f990ba --- /dev/null +++ b/mockup/patterns/structure/js/actions.js @@ -0,0 +1,129 @@ +define([ + 'jquery', + 'underscore', + 'backbone', + 'mockup-patterns-structure-url/js/models/result', + 'mockup-utils', + 'translate', +], function($, _, Backbone, Result, utils, _t) { + 'use strict'; + + // use a more primative class than Backbone.Model? + var Actions = Backbone.Model.extend({ + initialize: function(options) { + this.options = options; + this.app = options.app; + this.model = options.model; + this.selectedCollection = this.app.selectedCollection; + }, + selectAll: function(e){ + // This implementation is very specific to the default collection + // with the reliance on its queryParser and queryHelper. Custom + // collection (Backbone.Paginator.requestPager implementation) + // will have to come up with their own action for this. + e.preventDefault(); + var self = this; + var page = 1; + var count = 0; + var getPage = function(){ + self.app.loading.show(); + $.ajax({ + url: self.app.collection.url, + type: 'GET', + dataType: 'json', + data: { + query: self.app.collection.queryParser({ + searchPath: self.model.attributes.path + }), + batch: JSON.stringify({ + page: page, + size: 100 + }), + attributes: JSON.stringify( + self.app.collection.queryHelper.options.attributes) + } + }).done(function(data){ + var items = self.app.collection.parse(data, count); + count += items.length; + _.each(items, function(item){ + self.app.selectedCollection.add(new Result(item)); + }); + page += 1; + if(data.total > count){ + getPage(); + }else{ + self.app.loading.hide(); + self.app.tableView.render(); + } + }); + }; + getPage(); + }, + + doAction: function(buttonName, successMsg, failMsg){ + var self = this; + $.ajax({ + url: self.app.buttons.get(buttonName).options.url, + data: { + selection: JSON.stringify([self.model.attributes.UID]), + folder: self.model.attributes.path, + _authenticator: utils.getAuthenticator() + }, + dataType: 'json', + type: 'POST' + }).done(function(data){ + if(data.status === 'success'){ + self.app.setStatus(_t(successMsg + ' "' + self.model.attributes.Title + '"')); + self.app.collection.pager(); + self.app.updateButtons(); + }else{ + self.app.setStatus(_t('Error ' + failMsg + ' "' + self.model.attributes.Title + '"')); + } + }); + }, + + cutClicked: function(e) { + var self = this; + e.preventDefault(); + self.doAction('cut', _t('Cut'), _t('cutting')); + }, + copyClicked: function(e) { + var self = this; + e.preventDefault(); + self.doAction('copy', _t('Copied'), _t('copying')); + }, + pasteClicked: function(e) { + var self = this; + e.preventDefault(); + self.doAction('paste', _t('Pasted into'), _t('Error pasting into')); + }, + moveTopClicked: function(e) { + e.preventDefault(); + this.app.moveItem(this.model.attributes.id, 'top'); + }, + moveBottomClicked: function(e) { + e.preventDefault(); + this.app.moveItem(this.model.attributes.id, 'bottom'); + }, + setDefaultPageClicked: function(e) { + e.preventDefault(); + var self = this; + $.ajax({ + url: self.app.getAjaxUrl(self.app.setDefaultPageUrl), + type: 'POST', + data: { + '_authenticator': $('[name="_authenticator"]').val(), + 'id': this.model.attributes.id + }, + success: function(data) { + self.app.ajaxSuccessResponse.apply(self.app, [data]); + }, + error: function(data) { + self.app.ajaxErrorResponse.apply(self.app, [data]); + } + }); + }, + }); + + return Actions; +}); diff --git a/mockup/patterns/structure/js/collections/result.js b/mockup/patterns/structure/js/collections/result.js index 40cf6f625..74ca8fd23 100644 --- a/mockup/patterns/structure/js/collections/result.js +++ b/mockup/patterns/structure/js/collections/result.js @@ -2,20 +2,58 @@ define([ 'underscore', 'backbone', 'mockup-patterns-structure-url/js/models/result', + 'mockup-utils', 'backbone.paginator' -], function(_, Backbone, Result) { +], function(_, Backbone, Result, Utils) { 'use strict'; var ResultCollection = Backbone.Paginator.requestPager.extend({ model: Result, - queryHelper: null, // need to set initialize: function(models, options) { this.options = options; + this.view = options.view; this.url = options.url; - this.queryParser = options.queryParser; - this.queryHelper = options.queryHelper; + + this.queryHelper = Utils.QueryHelper( + $.extend(true, {}, this.view.options, { + attributes: this.view.options.queryHelperAttributes})); + + this.queryParser = function(options) { + var self = this; + if(options === undefined){ + options = {}; + } + var term = null; + if (self.view.toolbar) { + term = self.view.toolbar.get('filter').term; + } + var sortOn = self.view.sort_on; // jshint ignore:line + var sortOrder = self.view.sort_order; // jshint ignore:line + if (!sortOn) { + sortOn = 'getObjPositionInParent'; + } + return JSON.stringify({ + criteria: self.queryHelper.getCriterias(term, $.extend({}, options, { + additionalCriterias: self.view.additionalCriterias + })), + sort_on: sortOn, + sort_order: sortOrder + }); + } + + // check and see if a hash is provided for initial path + if (window.location.hash.substring(0, 2) === '#/') { + this.queryHelper.currentPath = window.location.hash.substring(1); + } + Backbone.Paginator.requestPager.prototype.initialize.apply(this, [models, options]); }, + getCurrentPath: function() { + return this.queryHelper.getCurrentPath(); + }, + setCurrentPath: function(path) { + this.queryHelper.currentPath = path; + }, pager: function() { this.trigger('pager'); Backbone.Paginator.requestPager.prototype.pager.apply(this, []); @@ -38,6 +76,9 @@ define([ // how many items per page should be shown perPage: 15 }, + // server_api are query parameters passed directly (currently GET + // parameters). These are currently generated using following + // functions. Renamed to queryParams in Backbone.Paginator 2.0. server_api: { query: function() { return this.queryParser(); diff --git a/mockup/patterns/structure/js/navigation.js b/mockup/patterns/structure/js/navigation.js new file mode 100644 index 000000000..882a11e09 --- /dev/null +++ b/mockup/patterns/structure/js/navigation.js @@ -0,0 +1,50 @@ +define([ + 'backbone', + 'mockup-utils', +], function(Backbone, utils) { + 'use strict'; + + // use a more primative class than Backbone.Model? + var Navigation = Backbone.Model.extend({ + initialize: function(options) { + this.options = options; + this.app = options.app; + this.model = options.model; + }, + + getSelectedBaseUrl: function() { + var self = this; + return self.model.attributes.getURL; + }, + openUrl: function(url) { + var self = this; + var win = utils.getWindow(); + var keyEvent = this.app.keyEvent; + if (keyEvent && keyEvent.ctrlKey) { + win.open(url); + } else { + win.location = url; + } + }, + openClicked: function(e) { + e.preventDefault(); + var self = this; + self.openUrl(self.getSelectedBaseUrl() + '/view'); + }, + editClicked: function(e) { + e.preventDefault(); + var self = this; + self.openUrl(self.getSelectedBaseUrl() + '/edit'); + }, + folderClicked: function(e) { + e.preventDefault(); + // handler for folder, go down path and show in contents window. + var self = this; + self.app.setCurrentPath(self.model.attributes.path); + // also switch to fix page in batch + self.app.collection.goTo(self.app.collection.information.firstPage); + }, + }); + + return Navigation; +}); diff --git a/mockup/patterns/structure/js/views/actionmenu.js b/mockup/patterns/structure/js/views/actionmenu.js index e5ae54821..ac2b71847 100644 --- a/mockup/patterns/structure/js/views/actionmenu.js +++ b/mockup/patterns/structure/js/views/actionmenu.js @@ -5,176 +5,80 @@ define([ 'mockup-ui-url/views/base', 'mockup-patterns-structure-url/js/models/result', 'mockup-utils', + 'mockup-patterns-structure-url/js/actions', + 'mockup-patterns-structure-url/js/actionmenu', 'text!mockup-patterns-structure-url/templates/actionmenu.xml', 'translate', 'bootstrap-dropdown' -], function($, _, Backbone, BaseView, Result, utils, ActionMenuTemplate, _t) { +], function($, _, Backbone, BaseView, Result, utils, Actions, ActionMenu, + ActionMenuTemplate, _t) { 'use strict'; - var ActionMenu = BaseView.extend({ + var ActionMenuView = BaseView.extend({ className: 'btn-group actionmenu', template: _.template(ActionMenuTemplate), - events: { - 'click .selectAll a': 'selectAll', - 'click .cutItem a': 'cutClicked', - 'click .copyItem a': 'copyClicked', - 'click .pasteItem a': 'pasteClicked', - 'click .move-top a': 'moveTopClicked', - 'click .move-bottom a': 'moveBottomClicked', - 'click .set-default-page a': 'setDefaultPageClicked', - 'click .openItem a': 'openClicked', - 'click .editItem a': 'editClicked' - }, - initialize: function(options) { - this.options = options; - this.app = options.app; - this.model = options.model; - this.selectedCollection = this.app.selectedCollection; - if (options.canMove === false){ - this.canMove = false; - }else { - this.canMove = true; - } - }, - selectAll: function(e){ - e.preventDefault(); + + // Static menu options + menuOptions: null, + // Dynamic menu options + menuGenerator: 'mockup-patterns-structure-url/js/actionmenu', + + eventConstructor: function(definition) { var self = this; - var page = 1; - var count = 0; - var getPage = function(){ - self.app.loading.show(); - $.ajax({ - url: self.app.collection.url, - type: 'GET', - dataType: 'json', - data: { - query: self.app.collection.queryParser({ - searchPath: self.model.attributes.path - }), - batch: JSON.stringify({ - page: page, - size: 100 - }), - attributes: JSON.stringify(self.app.queryHelper.options.attributes) - } - }).done(function(data){ - var items = self.app.collection.parse(data, count); - count += items.length; - _.each(items, function(item){ - self.app.selectedCollection.add(new Result(item)); - }); - page += 1; - if(data.total > count){ - getPage(); - }else{ - self.app.loading.hide(); - self.app.tableView.render(); - } - }); + var libName = definition[0], + method = definition[1]; + + if (!((typeof libName === 'string') && (typeof method === 'string'))) { + return false; + } + + var doEvent = function(e) { + var libCls = require(libName); + var lib = new libCls(self) + return lib[method] && lib[method](e); }; - getPage(); - }, - doAction: function(buttonName, successMsg, failMsg){ - var self = this; - $.ajax({ - url: self.app.buttons.get(buttonName).options.url, - data: { - selection: JSON.stringify([self.model.attributes.UID]), - folder: self.model.attributes.path, - _authenticator: utils.getAuthenticator() - }, - dataType: 'json', - type: 'POST' - }).done(function(data){ - if(data.status === 'success'){ - self.app.setStatus(_t(successMsg + ' "' + self.model.attributes.Title + '"')); - self.app.collection.pager(); - self.app.updateButtons(); - }else{ - self.app.setStatus(_t('Error ' + failMsg + ' "' + self.model.attributes.Title + '"')); - } - }); - }, - cutClicked: function(e) { - e.preventDefault(); - this.doAction('cut', _t('Cut'), _t('cutting')); - }, - copyClicked: function(e) { - e.preventDefault(); - this.doAction('copy', _t('Copied'), _t('copying')); - }, - pasteClicked: function(e) { - e.preventDefault(); - this.doAction('paste', _t('Pasted into'), _t('Error pasting into')); + return doEvent; }, - moveTopClicked: function(e) { - e.preventDefault(); - this.app.moveItem(this.model.attributes.id, 'top'); - }, - moveBottomClicked: function(e) { - e.preventDefault(); - this.app.moveItem(this.model.attributes.id, 'bottom'); - }, - setDefaultPageClicked: function(e) { - e.preventDefault(); + + events: function() { var self = this; - $.ajax({ - url: self.app.getAjaxUrl(self.app.setDefaultPageUrl), - type: 'POST', - data: { - '_authenticator': $('[name="_authenticator"]').val(), - 'id': this.model.attributes.id - }, - success: function(data) { - self.app.ajaxSuccessResponse.apply(self.app, [data]); - }, - error: function(data) { - self.app.ajaxErrorResponse.apply(self.app, [data]); + var result = {}; + _.each(self.menuOptions, function(menuOption, idx) { + var e = self.eventConstructor(menuOption); + if (e) { + result['click .' + idx + ' a'] = e; } }); + return result; }, - getSelectedBaseUrl: function() { - var self = this; - return self.model.attributes.getURL; - }, - getWindow: function() { - var win = window; - if (win.parent !== window) { - win = win.parent; - } - return win; - }, - openUrl: function(url) { + + initialize: function(options) { var self = this; - var win = self.getWindow(); - var keyEvent = this.app.keyEvent; - if (keyEvent && keyEvent.ctrlKey) { - win.open(url); - } else { - win.location = url; + BaseView.prototype.initialize.apply(self, [options]); + self.options = options; + self.selectedCollection = self.app.selectedCollection; + + // Then acquire the constructor method if specified, and + var menuGenerator = self.options.menuGenerator || self.menuGenerator; + if (menuGenerator) { + var menuGen = require(menuGenerator); + // override those options here. All definition done here so + // that self.events() will return the right things. + var menuOptions = menuGen(self); + if (typeof menuOptions === 'object') { + // Only assign this if we got the right basic type. + self.menuOptions = menuOptions; + // Should warn otherwise. + } } }, - openClicked: function(e) { - e.preventDefault(); - var self = this; - self.openUrl(self.getSelectedBaseUrl() + '/view'); - }, - editClicked: function(e) { - e.preventDefault(); - var self = this; - self.openUrl(self.getSelectedBaseUrl() + '/edit'); - }, render: function() { var self = this; self.$el.empty(); var data = this.model.toJSON(); - data.attributes = self.model.attributes; - data.pasteAllowed = self.app.pasteAllowed; - data.canSetDefaultPage = self.app.setDefaultPageUrl; - data.inQueryMode = self.app.inQueryMode(); data.header = self.options.header || null; - data.canMove = self.canMove; + data.menuOptions = self.menuOptions; self.$el.html(self.template($.extend({ _t: _t, @@ -191,5 +95,5 @@ define([ } }); - return ActionMenu; + return ActionMenuView; }); diff --git a/mockup/patterns/structure/js/views/app.js b/mockup/patterns/structure/js/views/app.js index 94f78f9c9..31d186c8f 100644 --- a/mockup/patterns/structure/js/views/app.js +++ b/mockup/patterns/structure/js/views/app.js @@ -25,7 +25,7 @@ define([ TableView, SelectionWellView, GenericPopover, RearrangeView, SelectionButtonView, PagingView, ColumnsView, TextFilterView, UploadView, - ResultCollection, SelectedCollection, utils, _t, logger) { + _ResultCollection, SelectedCollection, utils, _t, logger) { 'use strict'; var log = logger.getLogger('pat-structure'); @@ -44,6 +44,7 @@ define([ BaseView.prototype.initialize.apply(self, [options]); self.loading = new utils.Loading(); self.loading.show(); + self.pasteAllowed = $.cookie('__cp'); /* close popovers when clicking away */ $(document).click(function(e){ @@ -64,34 +65,17 @@ define([ } }); + var ResultCollection = require(options.collectionConstructor); + self.collection = new ResultCollection([], { + // Due to default implementation need to poke at things in here, + // view is passed. + view: self, url: self.options.collectionUrl, - queryParser: function(options) { - if(options === undefined){ - options = {}; - } - var term = null; - if (self.toolbar) { - term = self.toolbar.get('filter').term; - } - var sortOn = self['sort_on']; // jshint ignore:line - if (!sortOn) { - sortOn = 'getObjPositionInParent'; - } - return JSON.stringify({ - criteria: self.queryHelper.getCriterias(term, $.extend({}, options, { - additionalCriterias: self.additionalCriterias - })), - sort_on: sortOn, - sort_order: self['sort_order'] // jshint ignore:line - }); - }, - queryHelper: self.options.queryHelper }); self.setAllCookieSettings(); - self.queryHelper = self.options.queryHelper; self.selectedCollection = new SelectedCollection(); self.tableView = new TableView({app: self}); @@ -141,43 +125,82 @@ define([ self.loading.show(); self.updateButtons(); + // the remaining calls are related to window.pushstate. + // abort if feature unavailable. + if (!(window.history && window.history.pushState)) { + return + } + + // undo the flag set by popState to prevent the push state + // from being triggered here, and early abort out of the + // function to not execute the folowing pushState logic. + if (self.doNotPushState) { + self.doNotPushState = false; + return + } + + var path = self.getCurrentPath(); + if (path === '/'){ + path = ''; + } /* maintain history here */ - if(self.options.urlStructure && window.history && window.history.pushState){ - if (!self.doNotPushState){ - var path = self.queryHelper.getCurrentPath(); - if(path === '/'){ - path = ''; - } - var url = self.options.urlStructure.base + path + self.options.urlStructure.appended; - window.history.pushState(null, null, url); - $('body').trigger('structure-url-changed', path); - }else{ - self.doNotPushState = false; - } + if (self.options.pushStateUrl) { + // permit an extra slash in pattern, but strip that if there + // as path always will be prefixed with a `/` + var pushStateUrl = self.options.pushStateUrl.replace( + '/{path}', '{path}'); + var url = pushStateUrl.replace('{path}', path); + window.history.pushState(null, null, url); + } else if (self.options.urlStructure) { + // fallback to urlStructure specification + var url = self.options.urlStructure.base + path + self.options.urlStructure.appended; + window.history.pushState(null, null, url); + } + + if (self.options.traverseView) { + // flag specifies that the context view implements a traverse + // view (i.e. IPublishTraverse) and the path is a virtual path + // of some kind - use the base object instead for that by not + // specifying a path. + path = ''; + // TODO figure out whether the following event after this is + // needed at all. } + $('body').trigger('structure-url-changed', path); + }); - if (self.options.urlStructure && utils.featureSupport.history()){ + if ((self.options.pushStateUrl || self.options.urlStructure) + && utils.featureSupport.history()){ $(window).bind('popstate', function () { /* normalize this url first... */ - var url = window.location.href; + var win = utils.getWindow(); + var url = win.location.href; + var base, appended; if(url.indexOf('?') !== -1){ url = url.split('?')[0]; } if(url.indexOf('#') !== -1){ url = url.split('#')[0]; } + if (self.options.pushStateUrl) { + var tmp = self.options.pushStateUrl.split('{path}'); + base = tmp[0]; + appended = tmp[1]; + } else { + base = self.options.urlStructure.base; + appended = self.options.urlStructure.appended; + } // take off the base url - var path = url.substring(self.options.urlStructure.base.length); - if(path.substring(path.length - self.options.urlStructure.appended.length) === - self.options.urlStructure.appended){ + var path = url.substring(base.length); + if(path.substring(path.length - appended.length) === appended){ /* check that it ends with appended value */ - path = path.substring(0, path.length - self.options.urlStructure.appended.length); + path = path.substring(0, path.length - appended.length); } if(!path){ path = '/'; } - self.queryHelper.currentPath = path; + self.setCurrentPath(path); $('body').trigger('structure-url-changed', path); // since this next call causes state to be pushed... self.doNotPushState = true; @@ -199,11 +222,13 @@ define([ self.buttons.disable(); } - self.pasteAllowed = !!$.cookie('__cp'); - if (self.pasteAllowed) { - self.buttons.get('paste').enable(); - }else{ - self.buttons.get('paste').disable(); + if ('paste' in self.buttons) { + self.pasteAllowed = !!$.cookie('__cp'); + if (self.pasteAllowed) { + self.buttons.get('paste').enable(); + }else{ + self.buttons.get('paste').disable(); + } } }, inQueryMode: function() { @@ -229,8 +254,14 @@ define([ }); return uids; }, + getCurrentPath: function() { + return this.collection.getCurrentPath(); + }, + setCurrentPath: function(path) { + this.collection.setCurrentPath(path); + }, getAjaxUrl: function(url) { - return url.replace('{path}', this.options.queryHelper.getCurrentPath()); + return url.replace('{path}', this.getCurrentPath()); }, buttonClickEvent: function(button) { var self = this; @@ -261,7 +292,7 @@ define([ } data._authenticator = utils.getAuthenticator(); if (data.folder === undefined) { - data.folder = self.options.queryHelper.getCurrentPath(); + data.folder = self.getCurrentPath(); } var url = self.getAjaxUrl(button.url); @@ -467,7 +498,8 @@ define([ ); }, setAllCookieSettings: function() { - this.activeColumns = this.getCookieSetting('activeColumns', this.activeColumns); + this.activeColumns = this.getCookieSetting(this['activeColumnsCookie'], + this.activeColumns); var perPage = this.getCookieSetting('perPage', 15); if(typeof(perPage) === 'string'){ perPage = parseInt(perPage); diff --git a/mockup/patterns/structure/js/views/columns.js b/mockup/patterns/structure/js/views/columns.js index 3784ac445..cd8c11fcc 100644 --- a/mockup/patterns/structure/js/views/columns.js +++ b/mockup/patterns/structure/js/views/columns.js @@ -63,7 +63,7 @@ define([ self.$('input:checked').each(function() { self.app.activeColumns.push($(this).val()); }); - self.app.setCookieSetting('activeColumns', this.app.activeColumns); + self.app.setCookieSetting(self.app.activeColumnsCookie, this.app.activeColumns); self.app.tableView.render(); } }); diff --git a/mockup/patterns/structure/js/views/table.js b/mockup/patterns/structure/js/views/table.js index 399bb1570..bdf124fe6 100644 --- a/mockup/patterns/structure/js/views/table.js +++ b/mockup/patterns/structure/js/views/table.js @@ -12,7 +12,7 @@ define([ 'translate', 'bootstrap-alert' ], function($, _, Backbone, TableRowView, TableTemplate, BaseView, Sortable, - Moment, Result, ActionMenu, _t) { + Moment, Result, ActionMenuView, _t) { 'use strict'; var TableView = BaseView.extend({ @@ -62,9 +62,11 @@ define([ if (self.selectedCollection.findWhere({UID: data.object.UID})){ $('input[type="checkbox"]', self.$breadcrumbs)[0].checked = true; } - self.folderMenu = new ActionMenu({ + self.folderMenu = new ActionMenuView({ app: self.app, model: self.folderModel, + menuOptions: self.app.menuOptions, + menuGenerator: self.app.menuGenerator, header: _t('Actions on current folder'), canMove: false }); @@ -78,7 +80,7 @@ define([ self.$el.html(self.template({ _t: _t, pathParts: _.filter( - self.app.queryHelper.getCurrentPath().split('/').slice(1), + self.app.getCurrentPath().split('/').slice(1), function(val) { return val.length > 0; } @@ -105,7 +107,11 @@ define([ selector: '.ModificationDate,.EffectiveDate,.CreationDate,.ExpirationDate', format: self.options.app.momentFormat }); - self.addReordering(); + + if (self.app.options.moveUrl) { + self.addReordering(); + } + self.storeOrder(); return this; }, @@ -124,7 +130,7 @@ define([ } }); path += $el.attr('data-path'); - this.app.queryHelper.currentPath = path; + this.app.setCurrentPath(path); this.collection.pager(); }, selectFolder: function(e) { diff --git a/mockup/patterns/structure/js/views/tablerow.js b/mockup/patterns/structure/js/views/tablerow.js index 896d728da..5cda20e5b 100644 --- a/mockup/patterns/structure/js/views/tablerow.js +++ b/mockup/patterns/structure/js/views/tablerow.js @@ -2,11 +2,12 @@ define([ 'jquery', 'underscore', 'backbone', + 'mockup-patterns-structure-url/js/navigation', 'mockup-patterns-structure-url/js/views/actionmenu', 'text!mockup-patterns-structure-url/templates/tablerow.xml', 'mockup-utils', 'translate' -], function($, _, Backbone, ActionMenu, TableRowTemplate, utils, _t) { +], function($, _, Backbone, Nav, ActionMenuView, TableRowTemplate, utils, _t) { 'use strict'; var TableRowView = Backbone.View.extend({ @@ -30,9 +31,21 @@ define([ if (this.selectedCollection.findWhere({UID: data.UID})) { data.selected = true; } + if (!data.viewURL) { + // XXX + // This is for the new window link. There should also be a + // separate one for the default link and it shouldn't require a + // javascript function to append '/view' on the default click. + // Need actual documentation reference for this and also support + // from the vocabulary that generates the data for the default + // portal_contents view. + data.viewURL = data.getURL + '/view'; + } data.attributes = self.model.attributes; data.activeColumns = self.app.activeColumns; data.availableColumns = self.app.availableColumns; + data.portal_type = data.portal_type ? data.portal_type : ''; + data.contenttype = data.portal_type.toLowerCase().replace(/\.| /g, '-'); data._authenticator = utils.getAuthenticator(); data._t = _t; self.$el.html(self.template(data)); @@ -49,29 +62,44 @@ define([ self.el.model = this.model; - self.menu = new ActionMenu({ + var canMove = (!(!self.app.options.moveUrl)); + + self.menu = new ActionMenuView({ app: self.app, - model: self.model + model: self.model, + menuOptions: self.app.menuOptions, + menuGenerator: self.app.menuGenerator, + canMove: canMove }); $('.actionmenu-container', self.$el).append(self.menu.render().el); return this; }, itemClicked: function(e) { - e.preventDefault(); /* check if this should just be opened in new window */ + var self = this; var keyEvent = this.app.keyEvent; - if (keyEvent && keyEvent.ctrlKey) { - this.menu.openClicked(e); - } else if (this.model.attributes['is_folderish']) { // jshint ignore:line - // it's a folder, go down path and show in contents window. - this.app.queryHelper.currentPath = this.model.attributes.path; - // also switch to fix page in batch - var collection = this.app.collection; - collection.goTo(collection.information.firstPage); + var key; + // Resolve the correct handler based on these keys. + // Default handlers live in ../navigation.js (bound to Nav) + if (keyEvent && keyEvent.ctrlKey || + !(this.model.attributes['is_folderish'])) { + // middle/ctrl-click or not a folder content + key = 'other'; // default Nav.openClicked } else { - this.menu.openClicked(e); + key = 'folder'; // default Nav.folderClicked + } + var definition = self.app.options.tableRowItemAction[key] || []; + // a bit of a duplicate from actionmenu.js, but this is calling + // directly. + var libName = definition[0], + method = definition[1]; + if (!((typeof libName === 'string') && (typeof key === 'string'))) { + return null; } + var clsLib = require(libName); + var lib = new clsLib(self); + return lib[method] && lib[method](e); }, itemSelected: function() { var checkbox = this.$('input')[0]; diff --git a/mockup/patterns/structure/js/views/upload.js b/mockup/patterns/structure/js/views/upload.js index f9c5921e9..1ecb03f5f 100644 --- a/mockup/patterns/structure/js/views/upload.js +++ b/mockup/patterns/structure/js/views/upload.js @@ -34,7 +34,7 @@ define([ options.relatedItems = { vocabularyUrl: self.app.options.vocabularyUrl }; - options.currentPath = self.app.options.queryHelper.getCurrentPath(); + options.currentPath = self.app.getCurrentPath(); self.upload = new Upload(self.$('.uploadify-me').addClass('pat-upload'), options); return this; }, @@ -46,7 +46,7 @@ define([ if (!this.opened) { return; } - var currentPath = self.app.queryHelper.getCurrentPath(); + var currentPath = self.app.getCurrentPath(); var relatedItems = self.upload.relatedItems; if (self.currentPathData && relatedItems && currentPath !== self.upload.currentPath){ if (currentPath === '/'){ diff --git a/mockup/patterns/structure/pattern.js b/mockup/patterns/structure/pattern.js index acdf0f35a..d4e47316a 100644 --- a/mockup/patterns/structure/pattern.js +++ b/mockup/patterns/structure/pattern.js @@ -48,19 +48,41 @@ define([ indexOptionsUrl: null, // for querystring widget contextInfoUrl: null, // for add new dropdown and other info setDefaultPageUrl: null, + menuOptions: null, // default action menu options per item. + // default menu generator + menuGenerator: 'mockup-patterns-structure-url/js/actionmenu', backdropSelector: '.plone-modal', // Element upon which to apply backdrops used for popovers - attributes: [ + + activeColumnsCookie: 'activeColumns', + + /* + As the options operate on a merging basis per new attribute + (key/value pairs) on the option Object in a recursive fashion, + array items are also treated as Objects so that custom options + are replaced starting from index 0 up to the length of the + array. In the case of buttons, custom buttons are simply + replaced starting from the first one. The following defines the + customized attributes that should be replaced wholesale, with + the default version prefixed with `_default_`. + */ + + attributes: null, + _default_attributes: [ 'UID', 'Title', 'portal_type', 'path', 'review_state', 'ModificationDate', 'EffectiveDate', 'CreationDate', 'is_folderish', 'Subject', 'getURL', 'id', 'exclude_from_nav', 'getObjSize', 'last_comment_date', 'total_comments','getIcon' ], - activeColumns: [ + + activeColumns: null, + _default_activeColumns: [ 'ModificationDate', 'EffectiveDate', 'review_state' ], - availableColumns: { + + availableColumns: null, + _default_availableColumns: { 'id': 'ID', 'ModificationDate': 'Last modified', 'EffectiveDate': 'Published', @@ -75,6 +97,19 @@ define([ 'last_comment_date': 'Last comment date', 'total_comments': 'Total comments' }, + + // action triggered for the primary link for each table row. + tableRowItemAction: null, + _default_tableRowItemAction: { + folder: [ + 'mockup-patterns-structure-url/js/navigation', 'folderClicked'], + other: [ + 'mockup-patterns-structure-url/js/navigation', 'openClicked'] + }, + + collectionConstructor: + 'mockup-patterns-structure-url/js/collections/result', + momentFormat: 'relative', rearrange: { properties: { @@ -85,8 +120,9 @@ define([ }, basePath: '/', moveUrl: null, - buttons: [], - demoButtons: [{ + + buttons: null, + _default_buttons: [{ title: 'Cut', url: '/cut' },{ @@ -113,28 +149,48 @@ define([ title: 'Rename', url: '/rename' }], + upload: { uploadMultiple: true, showTitle: true } + }, init: function() { var self = this; - if(self.options.buttons.length === 0){ - /* XXX I know this is wonky... but this prevents - weird option merging issues */ - self.options.buttons = self.options.demoButtons; - } + + /* + This part replaces the undefined (null) values in the user + modifiable attributes with the default values. + + May want to consider moving the _default_* values out of the + options object. + */ + var replaceDefaults = [ + 'attributes', 'activeColumns', 'availableColumns', 'buttons']; + _.each(replaceDefaults, function(idx) { + if (self.options[idx] === null) { + self.options[idx] = self.options['_default_' + idx]; + } + }); + + var mergeDefaults = ['tableRowItemAction']; + _.each(mergeDefaults, function(idx) { + var old = self.options[idx]; + self.options[idx] = $.extend( + false, self.options['_default_' + idx], old + ); + }); + self.browsing = true; // so all queries will be correct with QueryHelper self.options.collectionUrl = self.options.vocabularyUrl; - self.options.queryHelper = new utils.QueryHelper( - $.extend(true, {}, self.options, {pattern: self})); + self.options.pattern = self; - // check and see if a hash is provided for initial path - if(window.location.hash.substring(0, 2) === '#/'){ - self.options.queryHelper.currentPath = window.location.hash.substring(1); - } - delete self.options.attributes; // not compatible with backbone + // the ``attributes`` options key is not compatible with backbone, + // but queryHelper that will be constructed by the default + // ResultCollection will expect this to be passed into it. + self.options.queryHelperAttributes = self.options.attributes; + delete self.options.attributes; self.view = new AppView(self.options); self.$el.append(self.view.render().$el); diff --git a/mockup/patterns/structure/templates/actionmenu.xml b/mockup/patterns/structure/templates/actionmenu.xml index a0c30abfd..830264ff0 100644 --- a/mockup/patterns/structure/templates/actionmenu.xml +++ b/mockup/patterns/structure/templates/actionmenu.xml @@ -8,24 +8,10 @@
  • <% } %> -
  • <%- _t("Cut") %>
  • -
  • <%- _t("Copy") %>
  • - <% if(pasteAllowed && attributes.is_folderish){ %> -
  • <%- _t("Paste") %>
  • - <% } %> - <% if(!inQueryMode && canMove){ %> -
  • <%- _t("Move to top of folder") %>
  • -
  • <%- _t("Move to bottom of folder") %>
  • - <% } %> - <% if(!attributes.is_folderish && canSetDefaultPage){ %> -
  • <%- _t("Set as default page") %>
  • - <% } %> - <% if(attributes.is_folderish){ %> -
  • <%- _t("Select all contained items") %>
  • - <% } %> - <% if(header) { %> -
  • <%- _t("Open") %>
  • - <% } %> -
  • <%- _t("Edit") %>
  • + + <% _.each(menuOptions, function(menuOption, idx){ %> +
  • <%- _t(menuOption[3]) %>
  • + <% }); %> + diff --git a/mockup/patterns/structure/templates/tablerow.xml b/mockup/patterns/structure/templates/tablerow.xml index 896761af0..29bc4a0e1 100644 --- a/mockup/patterns/structure/templates/tablerow.xml +++ b/mockup/patterns/structure/templates/tablerow.xml @@ -1,13 +1,14 @@ checked="checked" <% } %>/> - <%- Title %>
    <% if(attributes["getIcon"] ){ %>  <% } %> - +
    diff --git a/mockup/tests/pattern-structure-test.js b/mockup/tests/pattern-structure-test.js index f5eb5e3c9..3154cb70c 100644 --- a/mockup/tests/pattern-structure-test.js +++ b/mockup/tests/pattern-structure-test.js @@ -3,13 +3,31 @@ define([ 'jquery', 'pat-registry', 'mockup-patterns-structure', + 'mockup-patterns-structure-url/js/views/actionmenu', + 'mockup-patterns-structure-url/js/views/app', + 'mockup-patterns-structure-url/js/models/result', + 'mockup-utils', 'sinon', -], function(expect, $, registry, Structure, sinon) { +], function(expect, $, registry, Structure, ActionMenuView, AppView, Result, + utils, sinon) { 'use strict'; window.mocha.setup('bdd'); $.fx.off = true; + var structureUrlChangedPath = ''; + var dummyWindow = {}; + var history = { + 'pushState': function(data, title, url) { + history.pushed = { + data: data, + title: title, + url: url + }; + } + }; + dummyWindow.history = history; + function getQueryVariable(url, variable) { var query = url.split('?')[1]; if (query === undefined) { @@ -28,29 +46,360 @@ define([ var extraDataJsonItem = null; + /* ========================== + TEST: AppView constructor internal attribute/object correctness + ========================== */ + describe('AppView internals correctness', function() { + + it('AppView collection queryHelper attribute', function() { + /* + Since the test and dummy data provided later directly provides + that without actually consuming the query parameters that are + generated by the QueryHelper instance internal to this pattern, + it should be tested here. + + This is to ensure that if its construction is later changed + again it should at least trigger some failure and ensure that + the "fixed" version will continue to generate the correct + query parameters.. + */ + + var app = new AppView({ + // dummy pattern, extracted/referenced by QueryHelper + 'pattern': { + 'browsing': true, + 'basePath': '/' + }, + // the pattern accepts this as `attributes` but backbone doesn't + // accept this as a valid parameter, it must be renamed to be + // reused. The default ResultCollection implementation will + // then pass this back again as the `attributes` parameter to + // construct the internal QueryHelper instance that it owns. + 'queryHelperAttributes': ['foo', 'bar'], + + 'buttons': [{'title': 'Cut', 'url': '/cut'}], + 'activeColumns': [], + 'availableColumns': [], + 'indexOptionsUrl': '', + 'setDefaultPageUrl': '', + 'url': 'http://localhost:8081/vocab', + 'collectionConstructor': + 'mockup-patterns-structure-url/js/collections/result' + }); + + expect(app.collection.queryHelper.options.attributes).to.eql( + ['foo', 'bar']); + + expect(JSON.parse(app.collection.queryParser())).to.eql({ + "criteria": [{ + "i":"path", + "o":"plone.app.querystring.operation.string.path", + "v":"/::1" + }], + "sort_on":"getObjPositionInParent", + "sort_order":"ascending" + }); + + }); + }); + + + /* ========================== + TEST: Per Item Action Buttons + ========================== */ + describe('Per Item Action Buttons', function() { + beforeEach(function() { + this.server = sinon.fakeServer.create(); + this.server.autoRespond = true; + + this.server.respondWith('POST', '/cut', function (xhr, id) { + xhr.respond(200, { 'Content-Type': 'application/json' }, + JSON.stringify({ + status: 'success', + msg: 'cut' + })); + }); + + this.clock = sinon.useFakeTimers(); + + this.$el = $('
    ').appendTo('body'); + + this.app = new AppView({ + // XXX ActionButton need this lookup directly. + 'buttons': [{'title': 'Cut', 'url': '/cut'}], + + 'activeColumns': [], + 'availableColumns': [], + 'indexOptionsUrl': '', + 'setDefaultPageUrl': '', + 'collectionConstructor': + 'mockup-patterns-structure-url/js/collections/result', + }); + this.app.render(); + }); + + afterEach(function() { + this.clock.restore(); + this.server.restore(); + requirejs.undef('dummytestactions'); + requirejs.undef('dummytestactionmenu'); + }); + + it('basic action menu rendering', function() { + var model = new Result({ + "Title": "Dummy Object", + "is_folderish": true, + "review_state": "published" + }); + + var menu = new ActionMenuView({ + app: this.app, + model: model, + header: 'Menu Header' + }); + + var el = menu.render().el; + + expect($('li.dropdown-header', el).text()).to.equal('Menu Header'); + expect($('li a', el).length).to.equal(7); + expect($($('li a', el)[0]).text()).to.equal('Cut'); + + $('.cutItem a', el).click(); + this.clock.tick(500); + + expect(this.app.$('.status').text()).to.equal('Cut "Dummy Object"'); + + }); + + it('custom action menu items', function() { + var model = new Result({ + "Title": "Dummy Object", + "is_folderish": true, + "review_state": "published" + }); + + var menu = new ActionMenuView({ + app: this.app, + model: model, + menuOptions: { + 'cutItem': [ + 'mockup-patterns-structure-url/js/actions', + 'cutClicked', + '#', + 'Cut', + ], + }, + }); + + var el = menu.render().el; + expect($('li a', el).length).to.equal(1); + expect($($('li a', el)[0]).text()).to.equal('Cut'); + + $('.cutItem a', el).click(); + this.clock.tick(500); + expect(this.app.$('.status').text()).to.equal('Cut "Dummy Object"'); + + }); + + it('custom action menu items and actions.', function() { + // Define a custom dummy "module" + define('dummytestactions', ['backbone'], function(Backbone) { + var Actions = Backbone.Model.extend({ + initialize: function(options) { + this.options = options; + this.app = options.app; + }, + foobarClicked: function(e) { + var self = this; + self.app.setStatus('Status: foobar clicked'); + } + }); + return Actions; + }); + // use it to make it available synchronously. + require(['dummytestactions'], function(){}); + this.clock.tick(500); + + var model = new Result({ + "is_folderish": true, + "review_state": "published" + }); + + // Make use if that dummy in here. + var menu = new ActionMenuView({ + app: this.app, + model: model, + menuOptions: { + 'foobar': [ + 'dummytestactions', + 'foobarClicked', + '#', + 'Foo Bar', + ], + }, + }); + + var el = menu.render().el; + expect($('li a', el).length).to.equal(1); + expect($($('li a', el)[0]).text()).to.equal('Foo Bar'); + + $('.foobar a', el).click(); + this.clock.tick(500); + expect(this.app.$('.status').text()).to.equal('Status: foobar clicked'); + }); + + it('custom action menu actions missing.', function() { + // Define a custom dummy "module" + define('dummytestactions', ['backbone'], function(Backbone) { + var Actions = Backbone.Model.extend({ + initialize: function(options) { + this.options = options; + this.app = options.app; + }, + barbazClicked: function(e) { + var self = this; + self.app.setStatus('Status: barbaz clicked'); + } + }); + return Actions; + }); + + // use it to make it available synchronously. + require(['dummytestactions'], function(){}); + this.clock.tick(500); + + var model = new Result({ + "is_folderish": true, + "review_state": "published" + }); + + // Make use if that dummy in here. + var menu = new ActionMenuView({ + app: this.app, + model: model, + menuOptions: { + 'foobar': [ + 'dummytestactions', + 'foobarClicked', + '#', + 'Foo Bar', + ], + 'barbaz': [ + 'dummytestactions', + 'barbazClicked', + '#', + 'Bar Baz', + ], + }, + }); + + // Broken/missing action + var el = menu.render().el; + $('.foobar a', el).click(); + this.clock.tick(500); + expect(this.app.$('.status').text().trim()).to.equal(''); + }); + + it('custom action menu via generator.', function() { + // Define a custom dummy "module" + define('dummytestactions', ['backbone'], function(Backbone) { + var Actions = Backbone.Model.extend({ + initialize: function(options) { + this.options = options; + this.app = options.app; + }, + barbazClicked: function(e) { + var self = this; + self.app.setStatus('Status: barbaz clicked'); + } + }); + return Actions; + }); + + define('dummytestactionmenu', ['backbone'], function(Backbone) { + var ActionMenu = function(menu) { + return { + 'barbaz': [ + 'dummytestactions', + 'barbazClicked', + '#', + 'Bar Baz' + ] + }; + }; + return ActionMenu; + }); + // use them both to make it available synchronously. + require(['dummytestactions'], function(){}); + require(['dummytestactionmenu'], function(){}); + this.clock.tick(500); + + var model = new Result({ + "is_folderish": true, + "review_state": "published" + }); + + // Make use if that dummy in here. + var menu = new ActionMenuView({ + app: this.app, + model: model, + menuGenerator: 'dummytestactionmenu' + }); + + // Broken/missing action + var el = menu.render().el; + $('.barbaz a', el).click(); + this.clock.tick(500); + expect(this.app.$('.status').text().trim()).to.equal( + 'Status: barbaz clicked'); + }); + + }); + + /* ========================== TEST: Structure ========================== */ describe('Structure', function() { beforeEach(function() { // clear cookie setting + $.removeCookie('__cp'); $.removeCookie('_fc_perPage'); + $.removeCookie('_fc_activeColumns'); + $.removeCookie('_fc_activeColumnsCustom'); - this.$el = $('' + - '
    ' + - '
    ').appendTo('body'); + var structure = { + "vocabularyUrl": "/data.json", + "uploadUrl": "/upload", + "moveUrl": "/moveitem", + "indexOptionsUrl": "/tests/json/queryStringCriteria.json", + "contextInfoUrl": "{path}/contextInfo", + "setDefaultPageUrl": "/setDefaultPage", + "urlStructure": { + "base": "http://localhost:8081", + "appended": "/folder_contents" + } + }; + + this.$el = $('
    ').attr( + 'data-pat-structure', JSON.stringify(structure)).appendTo('body'); this.server = sinon.fakeServer.create(); this.server.autoRespond = true; + $('body').off('structure-url-changed').on('structure-url-changed', + function (e, path) { + structureUrlChangedPath = path; + } + ); + this.server.respondWith('GET', /data.json/, function (xhr, id) { var batch = JSON.parse(getQueryVariable(xhr.url, 'batch')); + var query = JSON.parse(getQueryVariable(xhr.url, 'query')); + var path = query.criteria[0].v.split(':')[0]; + if (path === '/') { + path = ''; + } var start = 0; var end = 15; if (batch) { @@ -59,9 +408,9 @@ define([ } var items = []; items.push({ - UID: '123sdfasdfFolder', - getURL: 'http://localhost:8081/folder', - path: '/folder', + UID: '123sdfasdf' + path + 'Folder', + getURL: 'http://localhost:8081' + path + '/folder', + path: path + '/folder', portal_type: 'Folder', Description: 'folder', Title: 'Folder', @@ -72,9 +421,9 @@ define([ }); for (var i = start; i < end; i = i + 1) { items.push({ - UID: '123sdfasdf' + i, - getURL: 'http://localhost:8081/item' + i, - path: '/item' + i, + UID: '123sdfasdf' + path + i, + getURL: 'http://localhost:8081' + path + '/item' + i, + path: path + '/item' + i, portal_type: 'Document ' + i, Description: 'document', Title: 'Document ' + i, @@ -106,6 +455,18 @@ define([ msg: 'pasted' })); }); + this.server.respondWith('POST', '/moveitem', function (xhr, id) { + xhr.respond(200, { 'Content-Type': 'application/json' }, JSON.stringify({ + status: 'success', + msg: 'moved ' + xhr.requestBody + })); + }); + this.server.respondWith('POST', '/setDefaultPage', function (xhr, id) { + xhr.respond(200, { 'Content-Type': 'application/json' }, JSON.stringify({ + status: 'success', + msg: 'defaulted' + })); + }); this.server.respondWith('GET', /contextInfo/, function (xhr, id) { var data = { addButtons: [{ @@ -135,16 +496,32 @@ define([ }); this.clock = sinon.useFakeTimers(); + + sinon.stub(utils, 'getWindow', function() { + return dummyWindow; + }); + + this.sandbox = sinon.sandbox.create(); + this.sandbox.stub(window, 'history', history); }); afterEach(function() { + // XXX QueryHelper behaves like a singleton as it pins self + // reference to the singleton instance of Utils within the + // requirejs framework, so its variables such as currentPath are + // persisted. Reset that here like so: + utils.QueryHelper({}).currentPath = '/'; extraDataJsonItem = null; this.server.restore(); this.clock.restore(); + this.sandbox.restore(); + $('body').html(''); + utils.getWindow.restore(); }); it('initialize', function() { registry.scan(this.$el); + // moveUrl provided, can get to this via order-support. expect(this.$el.find('.order-support > table').size()).to.equal(1); }); @@ -275,7 +652,8 @@ define([ $item.trigger('change'); this.clock.tick(1000); expect(this.$el.find('#btn-selected-items').html()).to.contain('16'); - + expect($('table tbody .selection input:checked', this.$el).length + ).to.equal(16); }); it('test unselect all', function() { @@ -286,10 +664,15 @@ define([ $item.trigger('change'); this.clock.tick(1000); expect(this.$el.find('#btn-selected-items').html()).to.contain('16'); + expect($('table tbody .selection input:checked', this.$el).length + ).to.equal(16); + $item[0].checked = false; $item.trigger('change'); this.clock.tick(1000); expect(this.$el.find('#btn-selected-items').html()).to.contain('0'); + expect($('table tbody .selection input:checked', this.$el).length + ).to.equal(0); }); it('test current folder buttons do not show on root', function() { @@ -321,5 +704,1586 @@ define([ expect(this.$el.find('#btn-selected-items').html()).to.contain('1'); }); + it('test displayed content', function() { + registry.scan(this.$el); + this.clock.tick(500); + + var $content_row = this.$el.find('table tbody tr').eq(0); + expect($content_row.find('td').length).to.equal(6); + expect($content_row.find('td').eq(1).text().trim()).to.equal('Folder'); + expect($content_row.find('td').eq(2).text().trim()).to.equal(''); + expect($content_row.find('td').eq(3).text().trim()).to.equal(''); + expect($content_row.find('td').eq(4).text().trim()).to.equal('published'); + expect($content_row.find('td .icon-group-right a').attr('href') + ).to.equal('http://localhost:8081/folder/view'); + + var $content_row1 = this.$el.find('table tbody tr').eq(1); + expect($content_row1.find('td').eq(1).text().trim()).to.equal( + 'Document 0'); + expect($content_row1.find('td .icon-group-right a').attr('href') + ).to.equal('http://localhost:8081/item0/view'); + }); + + it('test select all contained item action', function() { + registry.scan(this.$el); + this.clock.tick(1000); + + // Since the top level view doesn't currently provide 'Actions + // on current folder' action menu, go down one level. + var $item = this.$el.find('.itemRow').eq(0); + $('.title a', $item).trigger('click'); + this.clock.tick(1000); + + var menu = $('.fc-breadcrumbs-container .actionmenu', this.$el); + var options = $('ul li a', menu); + expect(options.length).to.equal(5); + + var selectAll = $('.selectAll a', menu); + expect(selectAll.text()).to.eql('Select all contained items'); + selectAll.trigger('click'); + this.clock.tick(1000); + expect($('table tbody .selection input:checked', this.$el).length + ).to.equal(16); + expect(this.$el.find('#btn-selected-items').html()).to.contain('101'); + }); + + it('test select displayed columns', function() { + registry.scan(this.$el); + this.clock.tick(500); + var $row = this.$el.find('table thead tr').eq(1); + expect($row.find('th').length).to.equal(6); + expect($row.find('th').eq(1).text()).to.equal('Title'); + expect($row.find('th').eq(2).text()).to.equal('Last modified'); + expect($row.find('th').eq(3).text()).to.equal('Published'); + expect($row.find('th').eq(4).text()).to.equal('Review state'); + expect($row.find('th').eq(5).text()).to.equal('Actions'); + + expect($.cookie('_fc_activeColumns')).to.be(undefined); + + this.$el.find('#btn-attribute-columns').trigger('click'); + this.clock.tick(500); + + var $checkbox = this.$el.find( + '.attribute-columns input[value="getObjSize"]'); + $checkbox[0].checked = true; + $checkbox.trigger('change'); + this.clock.tick(500); + + var $popover = this.$el.find('.popover.attribute-columns'); + expect($popover.find('button').text()).to.equal('Save'); + $popover.find('button').trigger('click'); + this.clock.tick(500); + + $row = this.$el.find('table thead tr').eq(1); + expect($row.find('th').length).to.equal(7); + expect($row.find('th').eq(5).text()).to.equal('Object Size'); + expect($row.find('th').eq(6).text()).to.equal('Actions'); + expect($.parseJSON($.cookie('_fc_activeColumns')).value).to.eql( + ["ModificationDate", "EffectiveDate", "review_state", "getObjSize"]); + + $checkbox[0].checked = false; + $checkbox.trigger('change'); + $popover.find('button').trigger('click'); + this.clock.tick(500); + + $row = this.$el.find('table thead tr').eq(1); + expect($row.find('th').length).to.equal(6); + expect($.parseJSON($.cookie('_fc_activeColumns')).value).to.eql( + ["ModificationDate", "EffectiveDate", "review_state"]); + + }); + + it('test main buttons count', function() { + registry.scan(this.$el); + this.clock.tick(1000); + var buttons = this.$el.find('#btngroup-mainbuttons a'); + expect(buttons.length).to.equal(8); + }); + + it('test itemRow default actionmenu folder', function() { + registry.scan(this.$el); + this.clock.tick(1000); + // folder + var folder = this.$el.find('.itemRow').eq(0); + expect(folder.data().id).to.equal('folder'); + expect($('.actionmenu ul li a', folder).length).to.equal(6); + // no pasting (see next test + expect($('.actionmenu ul li.pasteItem', folder).length).to.equal(0); + // no set default page + expect($('.actionmenu ul li.set-default-page a', folder).length + ).to.equal(0); + // can select all + expect($('.actionmenu ul li.selectAll', folder).text()).to.equal( + 'Select all contained items'); + }); + + it('test itemRow default actionmenu item', function() { + registry.scan(this.$el); + this.clock.tick(1000); + + var item = this.$el.find('.itemRow').eq(10); + expect(item.data().id).to.equal('item9'); + expect($('.actionmenu ul li a', item).length).to.equal(6); + // cannot select all + expect($('.actionmenu ul li.selectAll a', item).length).to.equal(0); + // can set default page + expect($('.actionmenu ul li.set-default-page', item).text()).to.equal( + 'Set as default page'); + $('.actionmenu ul li.set-default-page a', item).click(); + this.clock.tick(1000); + expect(this.$el.find('.order-support .status').html()).to.contain( + 'defaulted'); + }); + + it('test itemRow actionmenu paste click', function() { + // item pending to be pasted + $.cookie('__cp', 'dummy'); + this.clock.tick(1000); + registry.scan(this.$el); + this.clock.tick(1000); + // top item + var item0 = this.$el.find('.itemRow').eq(0); + expect(item0.data().id).to.equal('folder'); + expect($('.actionmenu ul li a', item0).length).to.equal(7); + expect($('.actionmenu ul li.pasteItem', item0).text()).to.equal('Paste'); + $('.actionmenu ul li.pasteItem a', item0).click(); + this.clock.tick(1000); + expect(this.$el.find('.order-support .status').html()).to.contain( + 'Pasted into "Folder"'); + }); + + it('test itemRow actionmenu move-top click', function() { + registry.scan(this.$el); + this.clock.tick(1000); + // top item + var item0 = this.$el.find('.itemRow').eq(0); + expect(item0.data().id).to.equal('folder'); + var item10 = this.$el.find('.itemRow').eq(10); + expect(item10.data().id).to.equal('item9'); + + expect($('.actionmenu ul li.move-top', item10).text()).to.equal( + 'Move to top of folder'); + $('.actionmenu ul li.move-top a', item10).trigger('click'); + this.clock.tick(1000); + + expect(this.$el.find('.order-support .status').html()).to.contain( + 'moved'); + expect(this.$el.find('.order-support .status').html()).to.contain( + 'delta=top'); + expect(this.$el.find('.order-support .status').html()).to.contain( + 'id=item9'); + // No items actually moved, this is to be implemented server-side. + }); + + it('test itemRow actionmenu selectAll click', function() { + registry.scan(this.$el); + this.clock.tick(1000); + + var folder = this.$el.find('.itemRow').eq(0); + $('.actionmenu ul li.selectAll a', folder).trigger('click'); + this.clock.tick(1000); + expect($('table tbody .selection input:checked', this.$el).length + ).to.equal(0); + // all items in the folder be populated within the selection well. + expect(this.$el.find('#btn-selected-items').html()).to.contain('101'); + }); + + it('test navigate to item', function() { + registry.scan(this.$el); + this.clock.tick(1000); + var pattern = this.$el.data('patternStructure'); + var item = this.$el.find('.itemRow').eq(10); + expect(item.data().id).to.equal('item9'); + $('.title a.manage', item).trigger('click'); + this.clock.tick(1000); + expect(dummyWindow.location).to.equal('http://localhost:8081/item9/view'); + + $('.actionmenu ul li.editItem a', item).trigger('click'); + this.clock.tick(1000); + expect(dummyWindow.location).to.equal('http://localhost:8081/item9/edit'); + }); + + it('test navigate to folder push states', function() { + registry.scan(this.$el); + this.clock.tick(1000); + var pattern = this.$el.data('patternStructure'); + var item = this.$el.find('.itemRow').eq(0); + expect(item.data().id).to.equal('folder'); + $('.title a.manage', item).trigger('click'); + this.clock.tick(1000); + expect(history.pushed.url).to.equal( + 'http://localhost:8081/folder/folder_contents'); + expect(structureUrlChangedPath).to.eql('/folder'); + + $('.fc-breadcrumbs a', this.$el).eq(0).trigger('click'); + this.clock.tick(1000); + expect(history.pushed.url).to.equal( + 'http://localhost:8081/folder_contents'); + expect(structureUrlChangedPath).to.eql(''); + }); + + it('test navigate to folder pop states', function() { + registry.scan(this.$el); + this.clock.tick(1000); + // Need to inject this to the mocked window location attribute the + // code will check against. This url is set before the trigger. + dummyWindow.location = { + 'href': 'http://localhost:8081/folder/folder/folder_contents'}; + // then trigger off the real window. + $(window).trigger('popstate'); + this.clock.tick(1000); + expect(structureUrlChangedPath).to.eql('/folder/folder'); + }); + }); + + /* ========================== + TEST: Structure Customized + ========================== */ + describe('Structure Customized', function() { + beforeEach(function() { + // clear cookie setting + $.removeCookie('_fc_perPage'); + + var structure = { + "vocabularyUrl": "/data.json", + "indexOptionsUrl": "/tests/json/queryStringCriteria.json", + "contextInfoUrl": "{path}/contextInfo", + "activeColumnsCookie": "activeColumnsCustom", + "buttons": [{ + "url": "foo", + "title": "Foo", + "id": "foo", + "icon": "" + }] + }; + + this.$el = $('
    ').attr( + 'data-pat-structure', JSON.stringify(structure)).appendTo('body'); + + this.server = sinon.fakeServer.create(); + this.server.autoRespond = true; + + this.server.respondWith('GET', /data.json/, function (xhr, id) { + var batch = JSON.parse(getQueryVariable(xhr.url, 'batch')); + var start = 0; + var end = 15; + if (batch) { + start = (batch.page - 1) * batch.size; + end = start + batch.size; + } + var items = []; + + xhr.respond(200, { 'Content-Type': 'application/json' }, JSON.stringify({ + total: 0, + results: items + })); + }); + this.server.respondWith('GET', /contextInfo/, function (xhr, id) { + var data = { + addButtons: [] + }; + if (xhr.url.indexOf('folder') !== -1){ + data.object = { + UID: '123sdfasdfFolder', + getURL: 'http://localhost:8081/folder', + path: '/folder', + portal_type: 'Folder', + Description: 'folder', + Title: 'Folder', + 'review_state': 'published', + 'is_folderish': true, + Subject: [], + id: 'folder' + }; + } + xhr.respond(200, { 'Content-Type': 'application/json' }, JSON.stringify(data)); + }); + + this.clock = sinon.useFakeTimers(); + + sinon.stub(utils, 'getWindow', function() { + return dummyWindow; + }); + + this.sandbox = sinon.sandbox.create(); + this.sandbox.stub(window, 'history', history); + }); + + afterEach(function() { + this.server.restore(); + this.clock.restore(); + this.sandbox.restore(); + $('body').html(''); + utils.getWindow.restore(); + $('body').off('structure-url-changed'); + }); + + it('initialize', function() { + registry.scan(this.$el); + // no order-support for this one due to lack of moveUrl + expect(this.$el.find('.order-support > table').size()).to.equal(0); + }); + + it('per page', function() { + registry.scan(this.$el); + this.clock.tick(1000); + this.$el.find('.serverhowmany15 a').trigger('click'); + this.clock.tick(1000); + expect(this.$el.find('.itemRow').length).to.equal(0); + this.$el.find('.serverhowmany30 a').trigger('click'); + this.clock.tick(1000); + expect(this.$el.find('.itemRow').length).to.equal(0); + }); + + it('test select all', function() { + registry.scan(this.$el); + this.clock.tick(1000); + var $item = this.$el.find('table th .select-all'); + $item[0].checked = true; + $item.trigger('change'); + this.clock.tick(1000); + expect(this.$el.find('#btn-selected-items').html()).to.contain('0'); + + }); + + it('test unselect all', function() { + registry.scan(this.$el); + this.clock.tick(1000); + var $item = this.$el.find('table th .select-all'); + $item[0].checked = true; + $item.trigger('change'); + this.clock.tick(1000); + expect(this.$el.find('#btn-selected-items').html()).to.contain('0'); + $item[0].checked = false; + $item.trigger('change'); + this.clock.tick(1000); + expect(this.$el.find('#btn-selected-items').html()).to.contain('0'); + }); + + it('test select displayed columns', function() { + registry.scan(this.$el); + // manually setting a borrowed cookie from the previous test. + $.cookie('_fc_activeColumns', + '{"value":["ModificationDate","EffectiveDate","review_state",' + + '"getObjSize"]}'); + this.clock.tick(500); + var $row = this.$el.find('table thead tr').eq(1); + expect($row.find('th').length).to.equal(6); + expect($row.find('th').eq(5).text()).to.equal('Actions'); + + expect($.cookie('_fc_activeColumnsCustom')).to.be(undefined); + + this.$el.find('#btn-attribute-columns').trigger('click'); + this.clock.tick(500); + + var $checkbox = this.$el.find( + '.attribute-columns input[value="portal_type"]'); + $checkbox[0].checked = true; + $checkbox.trigger('change'); + this.clock.tick(500); + + var $popover = this.$el.find('.popover.attribute-columns'); + expect($popover.find('button').text()).to.equal('Save'); + $popover.find('button').trigger('click'); + this.clock.tick(500); + + $row = this.$el.find('table thead tr').eq(1); + expect($row.find('th').length).to.equal(7); + expect($row.find('th').eq(5).text()).to.equal('Type'); + expect($row.find('th').eq(6).text()).to.equal('Actions'); + expect($.parseJSON($.cookie('_fc_activeColumnsCustom')).value).to.eql( + ["ModificationDate", "EffectiveDate", "review_state", "portal_type"]); + // standard cookie unchanged. + expect($.parseJSON($.cookie('_fc_activeColumns')).value).to.eql( + ["ModificationDate", "EffectiveDate", "review_state", "getObjSize"]); + + $checkbox[0].checked = false; + $checkbox.trigger('change'); + $popover.find('button').trigger('click'); + this.clock.tick(500); + + $row = this.$el.find('table thead tr').eq(1); + expect($row.find('th').length).to.equal(6); + expect($.parseJSON($.cookie('_fc_activeColumnsCustom')).value).to.eql( + ["ModificationDate", "EffectiveDate", "review_state"]); + + }); + + it('test main buttons count', function() { + registry.scan(this.$el); + this.clock.tick(1000); + var buttons = this.$el.find('#btngroup-mainbuttons a'); + expect(buttons.length).to.equal(1); + }); + + }); + + + /* ========================== + TEST: Structure no buttons + ========================== */ + describe('Structure no buttons', function() { + beforeEach(function() { + // clear cookie setting + $.removeCookie('_fc_perPage'); + $.removeCookie('_fc_activeColumnsCustom'); + + var structure = { + "vocabularyUrl": "/data.json", + "indexOptionsUrl": "/tests/json/queryStringCriteria.json", + "contextInfoUrl": "{path}/contextInfo", + "activeColumnsCookie": "activeColumnsCustom", + "activeColumns": ["getObjSize"], + "availableColumns": { + "id": "ID", + "CreationDate": "Created", + "getObjSize": "Object Size" + }, + "buttons": [] + }; + + this.$el = $('
    ').attr( + 'data-pat-structure', JSON.stringify(structure)).appendTo('body'); + + this.server = sinon.fakeServer.create(); + this.server.autoRespond = true; + + this.server.respondWith('GET', /data.json/, function (xhr, id) { + var batch = JSON.parse(getQueryVariable(xhr.url, 'batch')); + var start = 0; + var end = 15; + if (batch) { + start = (batch.page - 1) * batch.size; + end = start + batch.size; + } + var items = []; + + xhr.respond(200, { 'Content-Type': 'application/json' }, JSON.stringify({ + total: 0, + results: items + })); + }); + this.server.respondWith('GET', /contextInfo/, function (xhr, id) { + var data = { + addButtons: [] + }; + if (xhr.url.indexOf('folder') !== -1){ + data.object = { + UID: '123sdfasdfFolder', + getURL: 'http://localhost:8081/folder', + path: '/folder', + portal_type: 'Folder', + Description: 'folder', + Title: 'Folder', + 'review_state': 'published', + 'is_folderish': true, + Subject: [], + id: 'folder' + }; + } + xhr.respond(200, { 'Content-Type': 'application/json' }, JSON.stringify(data)); + }); + + this.clock = sinon.useFakeTimers(); + }); + + afterEach(function() { + this.server.restore(); + this.clock.restore(); + $('body').html(''); + }); + + it('test main buttons count', function() { + registry.scan(this.$el); + this.clock.tick(1000); + var buttons = this.$el.find('#btngroup-mainbuttons a'); + expect(buttons.length).to.equal(0); + }); + + it('test select displayed columns', function() { + registry.scan(this.$el); + this.clock.tick(500); + var $row = this.$el.find('table thead tr').eq(1); + expect($row.find('th').length).to.equal(4); + expect($row.find('th').eq(1).text()).to.equal('Title'); + expect($row.find('th').eq(2).text()).to.equal('Object Size'); + expect($row.find('th').eq(3).text()).to.equal('Actions'); + }); + + }); + + /* ========================== + TEST: Structure barebone columns + ========================== */ + describe('Structure barebone columns', function() { + beforeEach(function() { + // clear cookie setting + $.removeCookie('_fc_perPage'); + $.removeCookie('_fc_activeColumnsCustom'); + + var structure = { + "vocabularyUrl": "/data.json", + "indexOptionsUrl": "/tests/json/queryStringCriteria.json", + "contextInfoUrl": "{path}/contextInfo", + "activeColumnsCookie": "activeColumnsCustom", + "activeColumns": [], + "availableColumns": { + "getURL": "URL", + }, + "buttons": [], + "attributes": [ + 'Title', 'getURL' + ] + }; + + this.$el = $('
    ').attr( + 'data-pat-structure', JSON.stringify(structure)).appendTo('body'); + + this.server = sinon.fakeServer.create(); + this.server.autoRespond = true; + + this.server.respondWith('GET', /data.json/, function (xhr, id) { + var batch = JSON.parse(getQueryVariable(xhr.url, 'batch')); + var start = 0; + var end = 15; + if (batch) { + start = (batch.page - 1) * batch.size; + end = start + batch.size; + } + var items = []; + items.push({ + /* + getURL: 'http://localhost:8081/folder', + Title: 'Folder', + id: 'folder' + */ + // 'portal_type', 'review_state', 'getURL' + + getURL: 'http://localhost:8081/folder', + Title: 'Folder', + // Other required fields. + id: 'folder', + UID: 'folder' + }); + for (var i = start; i < end; i = i + 1) { + items.push({ + /* + getURL: 'http://localhost:8081/item' + i, + Title: 'Document ' + i, + */ + + getURL: 'http://localhost:8081/item' + i, + Title: 'Document ' + i, + // Other required fields. + id: 'item' + i, + UID: 'item' + i + }); + } + + xhr.respond(200, { 'Content-Type': 'application/json' }, JSON.stringify({ + total: 100, + results: items + })); + }); + this.server.respondWith('GET', /contextInfo/, function (xhr, id) { + var data = { + addButtons: [] + }; + if (xhr.url.indexOf('folder') !== -1){ + data.object = { + UID: '123sdfasdfFolder', + getURL: 'http://localhost:8081/folder', + path: '/folder', + portal_type: 'Folder', + Description: 'folder', + Title: 'Folder', + 'review_state': 'published', + 'is_folderish': true, + Subject: [], + id: 'folder' + }; + } + xhr.respond(200, { 'Content-Type': 'application/json' }, JSON.stringify(data)); + }); + + this.clock = sinon.useFakeTimers(); + }); + + afterEach(function() { + this.server.restore(); + this.clock.restore(); + $('body').html(''); + }); + + it('per page', function() { + registry.scan(this.$el); + this.clock.tick(1000); + this.$el.find('.serverhowmany15 a').trigger('click'); + this.clock.tick(1000); + expect(this.$el.find('.itemRow').length).to.equal(16); + this.$el.find('.serverhowmany30 a').trigger('click'); + this.clock.tick(1000); + expect(this.$el.find('.itemRow').length).to.equal(31); + }); + + it('test itemRow actionmenu move-top none', function() { + registry.scan(this.$el); + this.clock.tick(1000); + // top item + var item = this.$el.find('.itemRow').eq(1); + expect(item.data().id).to.equal('item0'); + // Since no moveUrl, no move-top or move-bottom. + expect(item.find('.actionmenu .move-top a').length).to.equal(0); + expect(item.find('.actionmenu .move-bottom a').length).to.equal(0); + }); + + }); + + /* ========================== + TEST: Structure no action buttons + ========================== */ + describe('Structure no action buttons', function() { + beforeEach(function() { + // clear cookie setting + $.removeCookie('_fc_perPage'); + $.removeCookie('_fc_activeColumnsCustom'); + + var structure = { + "vocabularyUrl": "/data.json", + "indexOptionsUrl": "/tests/json/queryStringCriteria.json", + "contextInfoUrl": "{path}/contextInfo", + "activeColumnsCookie": "activeColumnsCustom", + "activeColumns": [], + "availableColumns": { + "getURL": "URL", + }, + "buttons": [], + "menuOptions": [], + "attributes": [ + 'Title', 'getURL' + ] + }; + + this.$el = $('
    ').attr( + 'data-pat-structure', JSON.stringify(structure)).appendTo('body'); + + this.server = sinon.fakeServer.create(); + this.server.autoRespond = true; + + this.server.respondWith('GET', /data.json/, function (xhr, id) { + var batch = JSON.parse(getQueryVariable(xhr.url, 'batch')); + var start = 0; + var end = 15; + if (batch) { + start = (batch.page - 1) * batch.size; + end = start + batch.size; + } + var items = []; + items.push({ + getURL: 'http://localhost:8081/folder', + Title: 'Folder', + id: 'folder', + UID: 'folder', + }); + for (var i = start; i < end; i = i + 1) { + items.push({ + getURL: 'http://localhost:8081/item' + i, + Title: 'Document ' + i, + id: 'item' + 1, + UID: 'item' + 1, + }); + } + + xhr.respond(200, {'Content-Type': 'application/json'}, JSON.stringify({ + total: 100, + results: items + })); + }); + this.server.respondWith('GET', /contextInfo/, function (xhr, id) { + var data = { + addButtons: [] + }; + if (xhr.url.indexOf('folder') !== -1){ + data.object = { + UID: '123sdfasdfFolder', + getURL: 'http://localhost:8081/folder', + path: '/folder', + portal_type: 'Folder', + Description: 'folder', + Title: 'Folder', + 'review_state': 'published', + 'is_folderish': true, + Subject: [], + id: 'folder' + }; + } + xhr.respond(200, { 'Content-Type': 'application/json' }, JSON.stringify(data)); + }); + + this.clock = sinon.useFakeTimers(); + }); + + afterEach(function() { + this.server.restore(); + this.clock.restore(); + $('body').html(''); + }); + + it('test itemRow actionmenu no options.', function() { + registry.scan(this.$el); + this.clock.tick(1000); + // top item + var item = this.$el.find('.itemRow').eq(1); + expect(item.data().id).to.equal('item1'); + // Since no moveUrl, no move-top or move-bottom. + expect(item.find('.actionmenu * a').length).to.equal(0); + }); + + }); + + /* ========================== + TEST: Structure alternative action buttons + ========================== */ + describe('Structure alternative action buttons and links', function() { + beforeEach(function() { + // clear cookie setting + $.removeCookie('_fc_perPage'); + $.removeCookie('_fc_activeColumnsCustom'); + + var structure = { + "vocabularyUrl": "/data.json", + "indexOptionsUrl": "/tests/json/queryStringCriteria.json", + "contextInfoUrl": "{path}/contextInfo", + "activeColumnsCookie": "activeColumnsCustom", + "activeColumns": [], + "availableColumns": { + "getURL": "URL", + }, + "buttons": [], + "menuOptions": { + 'action1': [ + 'dummytestaction', + 'option1', + '#', + 'Option 1', + ], + 'action2': [ + 'dummytestaction', + 'option2', + '#', + 'Option 2', + ], + }, + 'tableRowItemAction': { + 'other': ['dummytestaction', 'handleOther'], + }, + "attributes": [ + 'Title', 'getURL' + ], + }; + + this.$el = $('
    ').attr( + 'data-pat-structure', JSON.stringify(structure)).appendTo('body'); + + $('body').off('structure-url-changed').on('structure-url-changed', + function (e, path) { + structureUrlChangedPath = path; + } + ); + + this.server = sinon.fakeServer.create(); + this.server.autoRespond = true; + + this.server.respondWith('GET', /data.json/, function (xhr, id) { + var batch = JSON.parse(getQueryVariable(xhr.url, 'batch')); + var start = 0; + var end = 15; + if (batch) { + start = (batch.page - 1) * batch.size; + end = start + batch.size; + } + var items = [{ + getURL: 'http://localhost:8081/folder', + Title: 'Folder', + 'is_folderish': true, + path: '/folder', + id: 'folder' + }, { + getURL: 'http://localhost:8081/item', + Title: 'Item', + 'is_folderish': false, + path: '/item', + id: 'item' + }]; + + xhr.respond(200, {'Content-Type': 'application/json'}, JSON.stringify({ + total: 1, + results: items + })); + }); + this.server.respondWith('GET', /contextInfo/, function (xhr, id) { + var data = { + addButtons: [] + }; + if (xhr.url.indexOf('folder') !== -1){ + data.object = { + UID: '123sdfasdfFolder', + getURL: 'http://localhost:8081/folder', + path: '/folder', + portal_type: 'Folder', + Description: 'folder', + Title: 'Folder', + 'review_state': 'published', + 'is_folderish': true, + Subject: [], + id: 'folder' + }; + } + xhr.respond(200, { 'Content-Type': 'application/json' }, JSON.stringify(data)); + }); + + this.clock = sinon.useFakeTimers(); + this.sandbox = sinon.sandbox.create(); + this.sandbox.stub(window, 'history', history); + + sinon.stub(utils, 'getWindow', function() { + return dummyWindow; + }); + }); + + afterEach(function() { + requirejs.undef('dummytestaction'); + utils.getWindow.restore(); + this.sandbox.restore(); + this.server.restore(); + this.clock.restore(); + $('body').html(''); + $('body').off('structure-url-changed'); + }); + + it('test itemRow actionmenu custom options.', function() { + registry.scan(this.$el); + this.clock.tick(1000); + var item = this.$el.find('.itemRow').eq(0); + // Check for complete new options + expect($('.actionmenu * a', item).length).to.equal(2); + expect($('.actionmenu .action1 a', item).text()).to.equal('Option 1'); + expect($('.actionmenu .action2 a', item).text()).to.equal('Option 2'); + + define('dummytestaction', ['backbone'], function(Backbone) { + var Actions = Backbone.Model.extend({ + initialize: function(options) { + this.options = options; + this.app = options.app; + }, + option1: function(e) { + e.preventDefault(); + var self = this; + self.app.setStatus('Status: option1 selected'); + }, + option2: function(e) { + e.preventDefault(); + var self = this; + self.app.setStatus('Status: option2 selected'); + } + }); + return Actions; + }); + // preload the defined module to allow it be used synchronously. + require(['dummytestaction'], function(){}); + this.clock.tick(1000); + + $('.actionmenu .action1 a', item).trigger('click'); + this.clock.tick(1000); + // status will be set as defined. + expect($('.status').text()).to.contain('Status: option1 selected'); + + $('.actionmenu .action2 a', item).trigger('click'); + this.clock.tick(1000); + // status will be set as defined. + expect($('.status').text()).to.contain('Status: option2 selected'); + }); + + it('folder link not overriden', function() { + registry.scan(this.$el); + this.clock.tick(1000); + var item = this.$el.find('.itemRow').eq(0); + $('.title a.manage', item).trigger('click'); + this.clock.tick(1000); + // default action will eventually trigger this. + expect(this.$el.find('.context-buttons').length).to.equal(1); + }); + + it('item link triggered', function() { + define('dummytestaction', ['backbone'], function(Backbone) { + var Actions = Backbone.Model.extend({ + initialize: function(options) { + this.options = options; + this.app = options.app; + }, + handleOther: function(e) { + e.preventDefault(); + var self = this; + self.app.setStatus('Status: not a folder'); + } + }); + return Actions; + }); + // preload the defined module to allow it be used synchronously. + require(['dummytestaction'], function(){}); + + registry.scan(this.$el); + this.clock.tick(1000); + var item = this.$el.find('.itemRow').eq(1); + $('.title a.manage', item).trigger('click'); + this.clock.tick(1000); + // status will be set as defined. + expect($('.status').text()).to.contain('Status: not a folder'); + }); + + }); + + /* ========================== + TEST: Structure traverse subpath + ========================== */ + describe('Structure traverse links', function() { + beforeEach(function() { + // clear cookie setting + $.removeCookie('_fc_perPage'); + $.removeCookie('_fc_activeColumnsCustom'); + + this.structure = { + "vocabularyUrl": "/data.json", + "indexOptionsUrl": "/tests/json/queryStringCriteria.json", + "contextInfoUrl": "{path}/contextInfo", + "activeColumnsCookie": "activeColumnsCustom", + "activeColumns": [], + "availableColumns": { + "getURL": "URL", + }, + "buttons": [], + "attributes": [ + 'Title', 'getURL' + ], + "urlStructure": { + "base": "http://localhost:8081/traverse_view", + "appended": "" + }, + "pushStateUrl": "http://localhost:8081/traverse_view{path}", + "traverseView": true + }; + + $('body').off('structure-url-changed').on('structure-url-changed', + function (e, path) { + structureUrlChangedPath = path; + } + ); + + this.server = sinon.fakeServer.create(); + this.server.autoRespond = true; + + this.server.respondWith('GET', /data.json/, function (xhr, id) { + var batch = JSON.parse(getQueryVariable(xhr.url, 'batch')); + var start = 0; + var end = 15; + if (batch) { + start = (batch.page - 1) * batch.size; + end = start + batch.size; + } + var items = [{ + getURL: 'http://localhost:8081/folder', + Title: 'Folder', + 'is_folderish': true, + path: '/folder', + id: 'folder' + }, { + getURL: 'http://localhost:8081/item', + Title: 'Item', + 'is_folderish': false, + path: '/item', + id: 'item' + }]; + + xhr.respond(200, {'Content-Type': 'application/json'}, JSON.stringify({ + total: 1, + results: items + })); + }); + this.server.respondWith('GET', /contextInfo/, function (xhr, id) { + var data = { + addButtons: [] + }; + if (xhr.url.indexOf('folder') !== -1){ + data.object = { + UID: '123sdfasdfFolder', + getURL: 'http://localhost:8081/folder', + path: '/folder', + portal_type: 'Folder', + Description: 'folder', + Title: 'Folder', + 'review_state': 'published', + 'is_folderish': true, + Subject: [], + id: 'folder' + }; + } + xhr.respond(200, { 'Content-Type': 'application/json' }, JSON.stringify(data)); + }); + + this.clock = sinon.useFakeTimers(); + this.sandbox = sinon.sandbox.create(); + this.sandbox.stub(window, 'history', history); + + sinon.stub(utils, 'getWindow', function() { + return dummyWindow; + }); + }); + + afterEach(function() { + requirejs.undef('dummytestaction'); + utils.getWindow.restore(); + this.sandbox.restore(); + this.server.restore(); + this.clock.restore(); + $('body').html(''); + $('body').off('structure-url-changed'); + }); + + it('test navigate to folder push states - urlStructure', function() { + delete this.structure.pushStateUrl; + this.$el = $('
    ').attr( + 'data-pat-structure', JSON.stringify(this.structure)).appendTo('body'); + registry.scan(this.$el); + this.clock.tick(1000); + var pattern = this.$el.data('patternStructure'); + var item = this.$el.find('.itemRow').eq(0); + expect(item.data().id).to.equal('folder'); + $('.title a.manage', item).trigger('click'); + this.clock.tick(1000); + expect(history.pushed.url).to.equal( + 'http://localhost:8081/traverse_view/folder'); + expect(structureUrlChangedPath).to.eql(''); + + $('.fc-breadcrumbs a', this.$el).eq(0).trigger('click'); + this.clock.tick(1000); + expect(history.pushed.url).to.equal( + 'http://localhost:8081/traverse_view'); + }); + + it('test navigate to folder pop states - urlStructure', function() { + delete this.structure.pushStateUrl; + this.$el = $('
    ').attr( + 'data-pat-structure', JSON.stringify(this.structure)).appendTo('body'); + registry.scan(this.$el); + this.clock.tick(1000); + // Need to inject this to the mocked window location attribute the + // code will check against. This url is set before the trigger. + dummyWindow.location = { + 'href': 'http://localhost:8081/traverse_view/folder/folder'}; + // then trigger off the real window. + $(window).trigger('popstate'); + this.clock.tick(1000); + expect(structureUrlChangedPath).to.eql('/folder/folder'); + }); + + it('test navigate to folder push states - pushStateUrl', function() { + delete this.structure.urlStructure; + this.$el = $('
    ').attr( + 'data-pat-structure', JSON.stringify(this.structure)).appendTo('body'); + registry.scan(this.$el); + this.clock.tick(1000); + var pattern = this.$el.data('patternStructure'); + var item = this.$el.find('.itemRow').eq(0); + expect(item.data().id).to.equal('folder'); + $('.title a.manage', item).trigger('click'); + this.clock.tick(1000); + expect(history.pushed.url).to.equal( + 'http://localhost:8081/traverse_view/folder'); + expect(structureUrlChangedPath).to.eql(''); + + $('.fc-breadcrumbs a', this.$el).eq(0).trigger('click'); + this.clock.tick(1000); + expect(history.pushed.url).to.equal( + 'http://localhost:8081/traverse_view'); + }); + + it('test navigate to folder pop states - pushStateUrl', function() { + delete this.structure.urlStructure; + this.$el = $('
    ').attr( + 'data-pat-structure', JSON.stringify(this.structure)).appendTo('body'); + registry.scan(this.$el); + this.clock.tick(1000); + // Need to inject this to the mocked window location attribute the + // code will check against. This url is set before the trigger. + dummyWindow.location = { + 'href': 'http://localhost:8081/traverse_view/folder/folder'}; + // then trigger off the real window. + $(window).trigger('popstate'); + this.clock.tick(1000); + expect(structureUrlChangedPath).to.eql('/folder/folder'); + }); + + }); + + /* ========================== + TEST: Structure action menu generator + ========================== */ + describe('Structure alternative action buttons and links', function() { + beforeEach(function() { + // clear cookie setting + $.removeCookie('_fc_perPage'); + $.removeCookie('_fc_activeColumnsCustom'); + + var structure = { + "vocabularyUrl": "/data.json", + "indexOptionsUrl": "/tests/json/queryStringCriteria.json", + "contextInfoUrl": "{path}/contextInfo", + "activeColumnsCookie": "activeColumnsCustom", + "activeColumns": [], + "availableColumns": { + "getURL": "URL", + }, + "buttons": [], + "menuGenerator": 'dummyactionmenu', + 'tableRowItemAction': { + 'other': ['dummytestaction', 'handleOther'], + }, + "attributes": [ + 'Title', 'getURL' + ], + "traverseView": true + }; + + this.$el = $('
    ').attr( + 'data-pat-structure', JSON.stringify(structure)).appendTo('body'); + + $('body').off('structure-url-changed').on('structure-url-changed', + function (e, path) { + structureUrlChangedPath = path; + } + ); + + this.server = sinon.fakeServer.create(); + this.server.autoRespond = true; + + this.server.respondWith('GET', /data.json/, function (xhr, id) { + var batch = JSON.parse(getQueryVariable(xhr.url, 'batch')); + var start = 0; + var end = 15; + if (batch) { + start = (batch.page - 1) * batch.size; + end = start + batch.size; + } + var items = [{ + getURL: 'http://localhost:8081/folder', + Title: 'Folder', + 'is_folderish': true, + path: '/folder', + id: 'folder' + }, { + getURL: 'http://localhost:8081/item', + Title: 'Item', + 'is_folderish': false, + path: '/item', + id: 'item' + }]; + + xhr.respond(200, {'Content-Type': 'application/json'}, JSON.stringify({ + total: 2, + results: items + })); + }); + this.server.respondWith('GET', /contextInfo/, function (xhr, id) { + var data = { + addButtons: [] + }; + if (xhr.url.indexOf('folder') !== -1){ + data.object = { + UID: '123sdfasdfFolder', + getURL: 'http://localhost:8081/folder', + path: '/folder', + portal_type: 'Folder', + Description: 'folder', + Title: 'Folder', + 'review_state': 'published', + 'is_folderish': true, + Subject: [], + id: 'folder' + }; + } + xhr.respond(200, { 'Content-Type': 'application/json' }, JSON.stringify(data)); + }); + + this.clock = sinon.useFakeTimers(); + this.sandbox = sinon.sandbox.create(); + this.sandbox.stub(window, 'history', history); + }); + + afterEach(function() { + requirejs.undef('dummytestaction'); + requirejs.undef('dummyactionmenu'); + this.sandbox.restore(); + this.server.restore(); + this.clock.restore(); + $('body').html(''); + $('body').off('structure-url-changed'); + }); + + it('test itemRow actionmenu generated options from model.', function() { + define('dummytestaction', ['backbone'], function(Backbone) { + var Actions = Backbone.Model.extend({ + initialize: function(options) { + this.options = options; + this.app = options.app; + }, + folderClicker: function(e) { + e.preventDefault(); + var self = this; + self.app.setStatus('Status: folder clicked'); + }, + itemClicker: function(e) { + e.preventDefault(); + var self = this; + self.app.setStatus('Status: item clicked'); + } + }); + return Actions; + }); + + define('dummyactionmenu', [], function() { + var ActionMenu = function(menu) { + if (menu.model.attributes.id === 'item') { + return { + 'itemClicker': [ + 'dummytestaction', + 'itemClicker', + '#', + 'Item Clicker' + ] + }; + } else { + return { + 'folderClicker': [ + 'dummytestaction', + 'folderClicker', + '#', + 'Folder Clicker' + ] + }; + } + }; + return ActionMenu; + }); + + // preload the defined module to allow it be used synchronously. + require(['dummytestaction'], function(){}); + require(['dummyactionmenu'], function(){}); + + registry.scan(this.$el); + this.clock.tick(1000); + + var folder = this.$el.find('.itemRow').eq(0); + + // Check for complete new options + expect($('.actionmenu * a', folder).length).to.equal(1); + expect($('.actionmenu .folderClicker a', folder).text()).to.equal( + 'Folder Clicker'); + $('.actionmenu .folderClicker a', folder).trigger('click'); + this.clock.tick(1000); + // status will be set as defined. + expect($('.status').text()).to.contain('Status: folder clicked'); + + var item = this.$el.find('.itemRow').eq(1); + // Check for complete new options + expect($('.actionmenu * a', item).length).to.equal(1); + expect($('.actionmenu .itemClicker a', item).text()).to.equal( + 'Item Clicker'); + $('.actionmenu .itemClicker a', item).trigger('click'); + this.clock.tick(1000); + // status will be set as defined. + expect($('.status').text()).to.contain('Status: item clicked'); + + }); + + it('test itemRow actionmenu malform generation.', function() { + // Potential failure case where user defined ActionMenu function + // fails to return a result, causing undefined behavior. + define('dummytestaction', ['backbone'], function(Backbone) { + // Not testing clicking here so barebone definition here. + var Actions = Backbone.Model.extend({ + initialize: function(options) { + this.options = options; + this.app = options.app; + } + }); + return Actions; + }); + + define('dummyactionmenu', [], function() { + var ActionMenu = function(menu) { + // return undefined + }; + return ActionMenu; + }); + + // preload the defined module to allow it be used synchronously. + require(['dummytestaction'], function(){}); + require(['dummyactionmenu'], function(){}); + + registry.scan(this.$el); + this.clock.tick(1000); + + // ensure that the items have been properly generated. + expect(this.$el.find('.itemRow').length).to.equal(2); + + }); + + }); + + /* ========================== + TEST: Structure custom URLs + ========================== */ + describe('Structure custom data URLs', function() { + beforeEach(function() { + // clear cookie setting + $.removeCookie('__cp'); + $.removeCookie('_fc_perPage'); + $.removeCookie('_fc_activeColumns'); + $.removeCookie('_fc_activeColumnsCustom'); + + this.$el = $('' + + '
    ' + + '
    ').appendTo('body'); + + this.server = sinon.fakeServer.create(); + this.server.autoRespond = true; + + this.server.respondWith('GET', /data.json/, function (xhr, id) { + var batch = JSON.parse(getQueryVariable(xhr.url, 'batch')); + var start = 0; + var end = 15; + if (batch) { + start = (batch.page - 1) * batch.size; + end = start + batch.size; + } + var items = []; + items.push({ + UID: '123sdfasdfFolder', + getURL: 'http://localhost:8081/folder', + viewURL: 'http://localhost:8081/folder', + path: '/folder', + portal_type: 'Folder', + Description: 'folder', + Title: 'Folder', + 'review_state': 'published', + 'is_folderish': true, + Subject: [], + id: 'folder' + }); + for (var i = start; i < end; i = i + 1) { + items.push({ + UID: '123sdfasdf' + i, + getURL: 'http://localhost:8081/item' + i, + viewURL: 'http://localhost:8081/item' + i + '/item_view', + path: '/item' + i, + portal_type: 'Document ' + i, + Description: 'document', + Title: 'Document ' + i, + 'review_state': 'published', + 'is_folderish': false, + Subject: [], + id: 'item' + i + }); + } + + xhr.respond(200, { 'Content-Type': 'application/json' }, JSON.stringify({ + total: 100, + results: items + })); + }); + + this.server.respondWith('GET', /contextInfo/, function (xhr, id) { + var data = { + addButtons: [{ + id: 'document', + title: 'Document', + url: '/adddocument' + },{ + id: 'folder', + title: 'Folder' + }], + }; + if (xhr.url.indexOf('folder') !== -1){ + data.object = { + UID: '123sdfasdfFolder', + getURL: 'http://localhost:8081/folder', + path: '/folder', + portal_type: 'Folder', + Description: 'folder', + Title: 'Folder', + 'review_state': 'published', + 'is_folderish': true, + Subject: [], + id: 'folder' + }; + } + xhr.respond(200, { 'Content-Type': 'application/json' }, JSON.stringify(data)); + }); + + this.clock = sinon.useFakeTimers(); + + sinon.stub(utils, 'getWindow', function() { + return dummyWindow; + }); + + }); + + afterEach(function() { + this.server.restore(); + this.clock.restore(); + $('body').html(''); + utils.getWindow.restore(); + }); + + it('test displayed content', function() { + registry.scan(this.$el); + this.clock.tick(500); + + var $content_row = this.$el.find('table tbody tr').eq(0); + expect($content_row.find('td').length).to.equal(6); + expect($content_row.find('td').eq(1).text().trim()).to.equal('Folder'); + expect($content_row.find('td').eq(2).text().trim()).to.equal(''); + expect($content_row.find('td').eq(3).text().trim()).to.equal(''); + expect($content_row.find('td').eq(4).text().trim()).to.equal('published'); + expect($content_row.find('td .icon-group-right a').attr('href') + ).to.equal('http://localhost:8081/folder'); + + var $content_row1 = this.$el.find('table tbody tr').eq(1); + expect($content_row1.find('td').eq(1).text().trim()).to.equal( + 'Document 0'); + expect($content_row1.find('td .icon-group-right a').attr('href') + ).to.equal('http://localhost:8081/item0/item_view'); + }); + + }); + + /* ========================== + TEST: Structure data insufficient fields + ========================== */ + describe('Structure data insufficient fields', function() { + beforeEach(function() { + // clear cookie setting + $.removeCookie('_fc_perPage'); + $.removeCookie('_fc_activeColumnsCustom'); + + var structure = { + "vocabularyUrl": "/data.json", + "uploadUrl": "/upload", + "moveUrl": "/moveitem", + "indexOptionsUrl": "/tests/json/queryStringCriteria.json", + "contextInfoUrl": "{path}/contextInfo", + "setDefaultPageUrl": "/setDefaultPage", + "urlStructure": { + "base": "http://localhost:8081", + "appended": "/folder_contents" + } + }; + + this.$el = $('
    ').attr( + 'data-pat-structure', JSON.stringify(structure)).appendTo('body'); + + this.server = sinon.fakeServer.create(); + this.server.autoRespond = true; + + this.server.respondWith('GET', /data.json/, function (xhr, id) { + var batch = JSON.parse(getQueryVariable(xhr.url, 'batch')); + var start = 0; + var end = 15; + if (batch) { + start = (batch.page - 1) * batch.size; + end = start + batch.size; + } + var items = [{ + getURL: 'http://localhost:8081/folder', + Title: 'Folder', + 'is_folderish': true, + path: '/folder', + UID: 'folder', + id: 'folder' + }, { + getURL: 'http://localhost:8081/item', + Title: 'Item', + 'is_folderish': false, + path: '/item', + // omitting id but provide UID instead for this test + UID: 'item' + }]; + + xhr.respond(200, {'Content-Type': 'application/json'}, JSON.stringify({ + total: 1, + results: items + })); + }); + this.server.respondWith('GET', /contextInfo/, function (xhr, id) { + var data = { + addButtons: [] + }; + if (xhr.url.indexOf('folder') !== -1){ + data.object = { + UID: '123sdfasdfFolder', + getURL: 'http://localhost:8081/folder', + path: '/folder', + portal_type: 'Folder', + Description: 'folder', + Title: 'Folder', + 'review_state': 'published', + 'is_folderish': true, + Subject: [], + id: 'folder' + }; + } + xhr.respond(200, { 'Content-Type': 'application/json' }, JSON.stringify(data)); + }); + + this.clock = sinon.useFakeTimers(); + }); + + afterEach(function() { + this.server.restore(); + this.clock.restore(); + $('body').html(''); + }); + + it('test unselect all', function() { + registry.scan(this.$el); + this.clock.tick(1000); + var $item = this.$el.find('table th .select-all'); + $item[0].checked = true; + $item.trigger('change'); + this.clock.tick(1000); + expect(this.$el.find('#btn-selected-items').html()).to.contain('2'); + expect($('table tbody .selection input:checked', this.$el).length + ).to.equal(2); + + // XXX passing this test for now with bad data - uncheck cannot + // remove items without an id (but with UID specified), so this + // item cannot be unselected. + $item[0].checked = false; + $item.trigger('change'); + this.clock.tick(1000); + expect(this.$el.find('#btn-selected-items').html()).to.contain('1'); + expect($('table tbody .selection input:checked', this.$el).length + ).to.equal(1); + }); + + }); + }); diff --git a/package.json b/package.json index 0cc83630c..bd6542fc4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mockup", - "version": "2.1.4", + "version": "2.2.0", "description": "A collection of client side patterns for faster and easier web development", "homepage": "http://plone.github.io/mockup", "devDependencies": {