Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
175 changes: 175 additions & 0 deletions client/zarafa/hierarchy/data/StoreOrder.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
Ext.namespace('Zarafa.hierarchy.data');

/**
* @class Zarafa.hierarchy.data.StoreOrder
* @extends Ext.util.Observable
*
* Manager for the user-defined order of the mailboxes (stores) in the hierarchy.
*
* By default the hierarchy places the own store on top, the Public store at the
* bottom, and sorts all shared stores alphabetically in between (see
* {@link Zarafa.hierarchy.ui.TreeSorter}). This manager holds an explicit order for
* the shared stores, which is persisted in the user settings so it survives a reload
* and applies to every hierarchy tree in the client.
*
* Stores are identified by their {@link Zarafa.hierarchy.data.MAPIStoreRecord#user_name
* user_name} rather than by their store entryid, because that is the identifier the
* shared-store settings themselves are keyed on and it remains valid when a store is
* closed and opened again.
*
* @singleton
*/
Zarafa.hierarchy.data.StoreOrder = Ext.extend(Ext.util.Observable, {
/**
* The settings path in which the order is persisted. The value is an array of
* {@link #getStoreKey store keys}, ordered top to bottom.
* @property
* @type String
*/
settingsPath: 'zarafa/v1/contexts/hierarchy/store_order',

/**
* @constructor
*/
constructor: function()
{
this.addEvents(
/**
* @event change
* Fires after the order of the mailboxes has been changed. Hierarchy trees
* listen to this to re-sort themselves, as the order applies to all of them
* and not only to the tree in which the change was made.
* @param {Zarafa.hierarchy.data.StoreOrder} storeOrder This object
* @param {String[]} order The new order as a list of {@link #getStoreKey store keys}
*/
'change'
);

Zarafa.hierarchy.data.StoreOrder.superclass.constructor.call(this);
},

/**
* Obtain the identifier under which the given store is recorded in the order.
* @param {Zarafa.hierarchy.data.MAPIStoreRecord} mapiStore The store to identify
* @return {String} The key for the store, or an empty string when the store cannot
* be identified and can therefore not be ordered.
*/
getStoreKey: function(mapiStore)
{
if (!mapiStore) {
return '';
}

var userName = mapiStore.get('user_name');
return Ext.isString(userName) ? userName.toLowerCase() : '';
},

/**
* Check if the given store may be moved by the user. Only shared stores can be
* reordered; the own store remains pinned to the top of the hierarchy and the
* Public store to the bottom, which is the order the client has always had.
* @param {Zarafa.hierarchy.data.MAPIStoreRecord} mapiStore The store to check
* @return {Boolean} True when the store can be reordered
*/
isReorderable: function(mapiStore)
{
if (!mapiStore || !mapiStore.isSharedStore()) {
return false;
}

return !Ext.isEmpty(this.getStoreKey(mapiStore));
},

/**
* Obtain the currently persisted order.
* @return {String[]} The ordered list of {@link #getStoreKey store keys}
*/
getOrder: function()
{
var order = container.getSettingsModel().get(this.settingsPath, true);
return Array.isArray(order) ? order : [];
},

/**
* Obtain the position of the given store within the persisted order.
* @param {Zarafa.hierarchy.data.MAPIStoreRecord} mapiStore The store to look up
* @return {Number} The index of the store, or -1 when the store has no explicit position
*/
indexOf: function(mapiStore)
{
var key = this.getStoreKey(mapiStore);
if (Ext.isEmpty(key)) {
return -1;
}

return this.getOrder().indexOf(key);
},

/**
* Compare two stores based on the user-defined order. Stores which have an explicit
* position are placed before stores which have none, so a mailbox opened after the
* user last reordered the hierarchy appears at the bottom of the shared stores
* rather than in an arbitrary spot.
* @param {Zarafa.hierarchy.data.MAPIStoreRecord} store1 The first store to compare
* @param {Zarafa.hierarchy.data.MAPIStoreRecord} store2 The second store to compare
* @return {Number} -1 when store1 comes first, +1 when store2 comes first, 0 when
* neither store has an explicit position and the caller must decide the order itself.
*/
compareStores: function(store1, store2)
{
// Both stores must be ones the user may reorder. Without this the own store,
// which never has an explicit position, would sort *after* every shared store
// that has one - visible in a filtered tree such as the calendar folder list,
// where the own store's folders are ordinary top level nodes and are therefore
// not held in place by the IPM_SUBTREE rules in the sorter.
if (!this.isReorderable(store1) || !this.isReorderable(store2)) {
return 0;
}

var index1 = this.indexOf(store1);
var index2 = this.indexOf(store2);

if (index1 === index2) {
// Either both stores are unordered (-1), or the same store is compared
// with itself. Let the caller fall back to its own comparison.
return 0;
}

if (index1 === -1) {
return +1;
}

if (index2 === -1) {
return -1;
}

return index1 < index2 ? -1 : +1;
},

/**
* Persist a new order and inform all listeners. The complete sequence of
* {@link #isReorderable reorderable} stores is written, so the stored order is
* always a full description of what the user sees rather than a partial one.
* @param {String[]} keys The {@link #getStoreKey store keys} in their new order
*/
setOrder: function(keys)
{
var order = [];

for (var i = 0, len = keys.length; i < len; i++) {
// Guard against a store being listed twice, which would make the
// comparison in compareStores depend on which duplicate is found first.
// A store can legitimately have several top level nodes in a filtered
// tree - one per visible folder - so duplicates are expected input here.
if (!Ext.isEmpty(keys[i]) && order.indexOf(keys[i]) === -1) {
order.push(keys[i]);
}
}

container.getSettingsModel().set(this.settingsPath, order);

this.fireEvent('change', this, order);
}
});

Zarafa.hierarchy.data.StoreOrder = new Zarafa.hierarchy.data.StoreOrder();
33 changes: 33 additions & 0 deletions client/zarafa/hierarchy/ui/ContextMenu.js
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,27 @@ Zarafa.hierarchy.ui.ContextMenu = Ext.extend(Zarafa.core.ui.menu.ConditionalMenu
item.setDisabled(true);
}
}
}, {
text: _('Remove'),
iconCls: 'icon_store_close',
handler: this.onContextItemRemoveSharedStore,
scope: this,
beforeShow: function(item, record) {
// A shared mailbox opened as a whole is shown in a filtered tree - the
// calendar folder list, for instance - as its visible folders, with no
// IPM_SUBTREE node to hang "Close store" on and no shared-folder key to
// hang "Close folder" on. Without this item such a mailbox cannot be
// removed from that list at all.
var node = this.contextNode;
var isTopLevel = !!node && !!node.parentNode && node.parentNode.isRoot === true;

if (isTopLevel && !record.isIPMSubTree() && !record.isSharedFolder() &&
record.getMAPIStore().isSharedStore()) {
item.setDisabled(false);
} else {
item.setDisabled(true);
}
}
}, {
xtype: 'menuseparator'
}, {
Expand Down Expand Up @@ -703,6 +724,18 @@ Zarafa.hierarchy.ui.ContextMenu = Ext.extend(Zarafa.core.ui.menu.ConditionalMenu
folderstore.save(this.records);
},

/**
* Fires on selecting the "Remove" menu option from the contextmenu, which is offered on
* a top level folder of a shared mailbox that was opened as a whole. The mailbox is not
* opened per folder, so there is no single folder to close: removing the entry means
* closing the shared mailbox, which is what "Close store" does on the mailbox itself.
* @private
*/
onContextItemRemoveSharedStore: function()
{
this.onContextItemCloseStore();
},

/**
* Fires on selecting 'Reload' menu option from {@link Zarafa.hierarchy.ui.ContextMenu ContextMenu}
* Reloads {@link Zarafa.hierarchy.data.HierarchyStore HierarchyStore} and populates new data in {@link Zarafa.hierarchy.ui.Tree Tree}.
Expand Down
125 changes: 123 additions & 2 deletions client/zarafa/hierarchy/ui/HierarchyFolderDropZone.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,13 @@ Zarafa.hierarchy.ui.HierarchyFolderDropZone = Ext.extend(Zarafa.hierarchy.ui.Hie
var dropNode = dropEvent.dropNode;

if (Ext.isDefined(dropNode)) {
// Reordering the mailboxes is a drop between siblings by definition: every
// mailbox hangs off the same (invisible) root node. So it must be exempt from
// the same-parent check below, which exists for folders being moved.
if (Zarafa.hierarchy.ui.HierarchyFolderDropZone.isStoreReorderDrop(dropEvent)) {
return;
}

var targetNode = dropEvent.target;

switch (dropEvent.point) {
Expand Down Expand Up @@ -79,11 +86,23 @@ Zarafa.hierarchy.ui.HierarchyFolderDropZone = Ext.extend(Zarafa.hierarchy.ui.Hie
getDropPoint: function(e, n, dd)
{
var tn = n.node;
var dragNode = dd && dd.dragData ? dd.dragData.node : undefined;

if (tn.isRoot) {
return tn.allowChildren !== false ? 'append' : false; // always append for root
}
var dragEl = n.ddel;

// A mailbox is dropped between mailboxes, never into one, so it has no 'append'
// point. Split the node in half instead of in thirds, which the code below does
// to leave room for 'append' - otherwise the middle of every row would be dead.
// Kept below the isRoot check above: the root node's UI has no 'elNode', so
// reading its geometry would throw.
if (Zarafa.hierarchy.ui.HierarchyFolderDropZone.isReorderableNode(dragNode)) {
var middle = Ext.lib.Dom.getY(dragEl) + (dragEl.offsetHeight / 2);
return Ext.lib.Event.getPageY(e) < middle ? 'above' : 'below';
}

var t = Ext.lib.Dom.getY(dragEl), b = t + dragEl.offsetHeight;
var y = Ext.lib.Event.getPageY(e);
var noAppend = tn.allowChildren === false;
Expand Down Expand Up @@ -133,10 +152,112 @@ Zarafa.hierarchy.ui.HierarchyFolderDropZone = Ext.extend(Zarafa.hierarchy.ui.Hie
*/
isValidDropPoint: function(n, pt, dd, e, data)
{
var folder = n.node.getFolder();
var targetNode = n ? n.node : undefined;
var dropNode = data ? data.node : undefined;
var zone = Zarafa.hierarchy.ui.HierarchyFolderDropZone;

if (!targetNode || !Ext.isFunction(targetNode.getFolder)) {
return false;
}

// A top level node stands for a mailbox, so dragging one can only ever mean moving
// that mailbox to another position - never a folder move, even in a filtered tree
// where the node happens to be an ordinary folder. It may therefore only be
// dropped between other mailboxes which the user is allowed to reorder, and never
// appended into a folder, which would read as "move this mailbox into that folder"
// and has no meaning.
if (zone.isTopLevelNode(dropNode)) {
if (!zone.isReorderableNode(dropNode) || !zone.isReorderableNode(targetNode)) {
return false;
}

if (pt !== 'above' && pt !== 'below') {
return false;
}

return zone.superclass.isValidDropPoint.apply(this, arguments);
}

var folder = targetNode.getFolder();
if (folder.isDropTargetForFolders() === false) {
return false;
}
return Zarafa.hierarchy.ui.HierarchyFolderDropZone.superclass.isValidDropPoint.apply(this, arguments);

// Dropping a folder above or below the root node of a store means dropping it
// into that node's parent, which is the invisible root node of the hierarchy -
// not a folder at all. The drop was accepted and then failed in the move
// handler, which reads 'target.getFolder()' on a node that has no such method.
if ((pt === 'above' || pt === 'below') && !targetNode.parentNode.getFolder) {
return false;
}

return zone.superclass.isValidDropPoint.apply(this, arguments);
}
});

Ext.apply(Zarafa.hierarchy.ui.HierarchyFolderDropZone, {
/**
* Check if the given tree node sits at the top level of the hierarchy, i.e. directly
* below the invisible root node. Which folder that is depends on the tree: in an
* unfiltered tree it is the IPM_SUBTREE of a store, but in a filtered tree - the
* calendar folder list, for instance - the IPM_SUBTREE of a shared store is left out
* and its visible folders are lifted to the top level instead
* (see {@link Zarafa.hierarchy.data.HierarchyTreeLoader#directFn}).
* @param {Ext.tree.TreeNode} node The node to check
* @return {Boolean} True when the node is a top level node of the hierarchy
* @static
*/
isTopLevelNode: function(node)
{
if (!node || !Ext.isFunction(node.getFolder) || !node.getFolder()) {
return false;
}

if (!node.parentNode) {
return false;
}

return node.parentNode.isRoot === true;
},

/**
* Check if the given tree node stands for a mailbox whose position in the hierarchy
* the user is allowed to change. In a filtered tree several nodes can stand for the
* same mailbox, one per visible folder; each of them may be dragged and they move
* the mailbox as a whole.
* @param {Ext.tree.TreeNode} node The node to check
* @return {Boolean} True when dragging the node reorders the mailboxes
* @static
*/
isReorderableNode: function(node)
{
if (!this.isTopLevelNode(node)) {
return false;
}

return Zarafa.hierarchy.data.StoreOrder.isReorderable(node.getFolder().getMAPIStore());
},

/**
* Check if the given drop event describes a mailbox being moved to a new position in
* the hierarchy, as opposed to a folder being moved or copied. A drop onto another
* node of the same mailbox is not a reorder - there is nothing to move.
* @param {Object} dropEvent The event object which describes what node is being dropped where
* @return {Boolean} True when the drop reorders the mailboxes
* @static
*/
isStoreReorderDrop: function(dropEvent)
{
if (dropEvent.point !== 'above' && dropEvent.point !== 'below') {
return false;
}

if (!this.isReorderableNode(dropEvent.dropNode) || !this.isReorderableNode(dropEvent.target)) {
return false;
}

var storeOrder = Zarafa.hierarchy.data.StoreOrder;
return storeOrder.getStoreKey(dropEvent.dropNode.getFolder().getMAPIStore()) !==
storeOrder.getStoreKey(dropEvent.target.getFolder().getMAPIStore());
}
});
Loading