mirror of
https://github.com/nasa/openmct.git
synced 2024-12-24 07:16:39 +00:00
Merge pull request #775 from nasa/timeline-drag-drop-679
[Timeline] Move (instead of link) when dragging and dropping within a timeline
This commit is contained in:
commit
700e605bbd
@ -34,6 +34,8 @@ define([
|
||||
"./src/actions/SaveAction",
|
||||
"./src/actions/CancelAction",
|
||||
"./src/policies/EditActionPolicy",
|
||||
"./src/policies/EditableLinkPolicy",
|
||||
"./src/policies/EditableMovePolicy",
|
||||
"./src/policies/EditNavigationPolicy",
|
||||
"./src/representers/EditRepresenter",
|
||||
"./src/representers/EditToolbarRepresenter",
|
||||
@ -56,6 +58,8 @@ define([
|
||||
SaveAction,
|
||||
CancelAction,
|
||||
EditActionPolicy,
|
||||
EditableLinkPolicy,
|
||||
EditableMovePolicy,
|
||||
EditNavigationPolicy,
|
||||
EditRepresenter,
|
||||
EditToolbarRepresenter,
|
||||
@ -187,6 +191,14 @@ define([
|
||||
"category": "action",
|
||||
"implementation": EditActionPolicy
|
||||
},
|
||||
{
|
||||
"category": "action",
|
||||
"implementation": EditableMovePolicy
|
||||
},
|
||||
{
|
||||
"category": "action",
|
||||
"implementation": EditableLinkPolicy
|
||||
},
|
||||
{
|
||||
"category": "navigation",
|
||||
"message": "There are unsaved changes.",
|
||||
|
52
platform/commonUI/edit/src/policies/EditableLinkPolicy.js
Normal file
52
platform/commonUI/edit/src/policies/EditableLinkPolicy.js
Normal file
@ -0,0 +1,52 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT Web, Copyright (c) 2014-2015, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT Web is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT Web includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
/*global define*/
|
||||
|
||||
define([], function () {
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* Policy suppressing links when the linked-to domain object is in
|
||||
* edit mode. Domain objects being edited may not have been persisted,
|
||||
* so creating links to these can result in inconsistent state.
|
||||
*
|
||||
* @memberof platform/commonUI/edit
|
||||
* @constructor
|
||||
* @implements {Policy.<View, DomainObject>}
|
||||
*/
|
||||
function EditableLinkPolicy() {
|
||||
}
|
||||
|
||||
EditableLinkPolicy.prototype.allow = function (action, context) {
|
||||
var key = action.getMetadata().key;
|
||||
|
||||
if (key === 'link') {
|
||||
return !((context.selectedObject || context.domainObject)
|
||||
.hasCapability('editor'));
|
||||
}
|
||||
|
||||
// Like all policies, allow by default.
|
||||
return true;
|
||||
};
|
||||
|
||||
return EditableLinkPolicy;
|
||||
});
|
51
platform/commonUI/edit/src/policies/EditableMovePolicy.js
Normal file
51
platform/commonUI/edit/src/policies/EditableMovePolicy.js
Normal file
@ -0,0 +1,51 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT Web, Copyright (c) 2014-2015, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT Web is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT Web includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
/*global define*/
|
||||
|
||||
define([], function () {
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* Policy suppressing move actions among editable and non-editable
|
||||
* domain objects.
|
||||
* @memberof platform/commonUI/edit
|
||||
* @constructor
|
||||
* @implements {Policy.<View, DomainObject>}
|
||||
*/
|
||||
function EditableMovePolicy() {
|
||||
}
|
||||
|
||||
EditableMovePolicy.prototype.allow = function (action, context) {
|
||||
var domainObject = context.domainObject,
|
||||
selectedObject = context.selectedObject,
|
||||
key = action.getMetadata().key;
|
||||
|
||||
if (key === 'move' && domainObject.hasCapability('editor')) {
|
||||
return !!selectedObject && selectedObject.hasCapability('editor');
|
||||
}
|
||||
|
||||
// Like all policies, allow by default.
|
||||
return true;
|
||||
};
|
||||
|
||||
return EditableMovePolicy;
|
||||
});
|
@ -28,7 +28,8 @@ define(
|
||||
[],
|
||||
function () {
|
||||
"use strict";
|
||||
var DISALLOWED_ACTIONS = ["move", "copy", "link", "window", "follow"];
|
||||
var DISALLOWED_ACTIONS = ["copy", "window", "follow"];
|
||||
|
||||
/**
|
||||
* The ActionCapability allows applicable Actions to be retrieved and
|
||||
* performed for specific domain objects, e.g.:
|
||||
|
@ -74,12 +74,6 @@ define(
|
||||
return swimlane.children.map(matches).reduce(or, false);
|
||||
}
|
||||
|
||||
// Remove a domain object from its current location
|
||||
function remove(domainObject) {
|
||||
return domainObject &&
|
||||
domainObject.getCapability('action').perform('remove');
|
||||
}
|
||||
|
||||
// Initiate mutation of a domain object
|
||||
function doMutate(domainObject, mutator) {
|
||||
return asPromise(
|
||||
@ -106,6 +100,20 @@ define(
|
||||
return swimlane.highlight() || expandedForDropInto();
|
||||
}
|
||||
|
||||
|
||||
// Choose an appropriate composition action for the drag-and-drop
|
||||
function chooseAction(targetObject, droppedObject) {
|
||||
var actionCapability =
|
||||
targetObject.getCapability('action'),
|
||||
actionKey = droppedObject.hasCapability('editor') ?
|
||||
'move' : 'link';
|
||||
|
||||
return actionCapability && actionCapability.getActions({
|
||||
key: actionKey,
|
||||
selectedObject: droppedObject
|
||||
})[0];
|
||||
}
|
||||
|
||||
// Choose an index for insertion in a domain object's composition
|
||||
function chooseTargetIndex(id, offset, composition) {
|
||||
return Math.max(
|
||||
@ -121,6 +129,10 @@ define(
|
||||
function insert(id, target, indexOffset) {
|
||||
var myId = swimlane.domainObject.getId();
|
||||
return doMutate(target, function (model) {
|
||||
model.composition =
|
||||
model.composition.filter(function (compId) {
|
||||
return compId !== id;
|
||||
});
|
||||
model.composition.splice(
|
||||
chooseTargetIndex(myId, indexOffset, model.composition),
|
||||
0,
|
||||
@ -129,17 +141,27 @@ define(
|
||||
});
|
||||
}
|
||||
|
||||
// Check if a compose action is allowed for the object in this
|
||||
// swimlane (we handle the link differently to set the index,
|
||||
// but check for the existence of the action to invole the
|
||||
// relevant policies.)
|
||||
function allowsCompose(swimlane, domainObject) {
|
||||
var actionCapability =
|
||||
swimlane.domainObject.getCapability('action');
|
||||
return actionCapability && actionCapability.getActions({
|
||||
key: 'compose',
|
||||
selectedObject: domainObject
|
||||
}).length > 0;
|
||||
function canDrop(targetObject, droppedObject) {
|
||||
var droppedContext = droppedObject.getCapability('context'),
|
||||
droppedParent =
|
||||
droppedContext && droppedContext.getParent(),
|
||||
droppedParentId = droppedParent && droppedParent.getId();
|
||||
|
||||
return (targetObject.getId() === droppedParentId) ||
|
||||
!!chooseAction(targetObject, droppedObject);
|
||||
}
|
||||
|
||||
function drop(domainObject, targetObject, indexOffset) {
|
||||
var action = chooseAction(targetObject, domainObject);
|
||||
|
||||
function changeIndex() {
|
||||
var id = domainObject.getId();
|
||||
return insert(id, targetObject, indexOffset);
|
||||
}
|
||||
|
||||
return action ?
|
||||
action.perform().then(changeIndex) :
|
||||
changeIndex();
|
||||
}
|
||||
|
||||
return {
|
||||
@ -154,7 +176,7 @@ define(
|
||||
return inEditMode() &&
|
||||
!pathContains(swimlane, id) &&
|
||||
!contains(swimlane, id) &&
|
||||
allowsCompose(swimlane, domainObject);
|
||||
canDrop(swimlane.domainObject, domainObject);
|
||||
},
|
||||
/**
|
||||
* Check if a drop-after should be allowed for this swimlane,
|
||||
@ -169,7 +191,7 @@ define(
|
||||
return inEditMode() &&
|
||||
target &&
|
||||
!pathContains(target, id) &&
|
||||
allowsCompose(target, domainObject);
|
||||
canDrop(target.domainObject, domainObject);
|
||||
},
|
||||
/**
|
||||
* Drop the provided domain object into a timeline. This is
|
||||
@ -192,11 +214,7 @@ define(
|
||||
Number.POSITIVE_INFINITY;
|
||||
|
||||
if (swimlane.highlight() || swimlane.highlightBottom()) {
|
||||
// Remove the domain object from its original location...
|
||||
return asPromise(remove(domainObject)).then(function () {
|
||||
// ...then insert it at its new location.
|
||||
insert(id, dropTarget, dropIndexOffset);
|
||||
});
|
||||
return drop(domainObject, dropTarget, dropIndexOffset);
|
||||
}
|
||||
}
|
||||
};
|
||||
@ -204,4 +222,4 @@ define(
|
||||
|
||||
return TimelineSwimlaneDropHandler;
|
||||
}
|
||||
);
|
||||
);
|
||||
|
@ -82,12 +82,17 @@ define(
|
||||
),
|
||||
draggedSwimlane = dndService.getData(
|
||||
SwimlaneDragConstants.TIMELINE_SWIMLANE_DRAG_TYPE
|
||||
);
|
||||
),
|
||||
droppedObject = draggedSwimlane ?
|
||||
draggedSwimlane.domainObject :
|
||||
dndService.getData(
|
||||
SwimlaneDragConstants.MCT_EXTENDED_DRAG_TYPE
|
||||
);
|
||||
|
||||
if (id) {
|
||||
event.stopPropagation();
|
||||
// Delegate the drop to the swimlane itself
|
||||
swimlane.drop(id, (draggedSwimlane || {}).domainObject);
|
||||
swimlane.drop(id, droppedObject);
|
||||
}
|
||||
|
||||
// Clear the swimlane highlights
|
||||
|
@ -31,9 +31,13 @@ define(
|
||||
mockOtherObject,
|
||||
mockActionCapability,
|
||||
mockPersistence,
|
||||
mockContext,
|
||||
mockAction,
|
||||
handler;
|
||||
|
||||
beforeEach(function () {
|
||||
var mockPromise = jasmine.createSpyObj('promise', ['then']);
|
||||
|
||||
mockSwimlane = jasmine.createSpyObj(
|
||||
"swimlane",
|
||||
[ "highlight", "highlightBottom" ]
|
||||
@ -60,6 +64,11 @@ define(
|
||||
[ "getId", "getCapability", "useCapability", "hasCapability" ]
|
||||
);
|
||||
|
||||
mockAction = jasmine.createSpyObj('action', ['perform']);
|
||||
mockAction.perform.andReturn(mockPromise);
|
||||
mockPromise.then.andCallFake(function (callback) {
|
||||
callback();
|
||||
});
|
||||
|
||||
mockOtherObject = jasmine.createSpyObj(
|
||||
"domainObject",
|
||||
@ -67,20 +76,34 @@ define(
|
||||
);
|
||||
mockActionCapability = jasmine.createSpyObj("action", ["perform", "getActions"]);
|
||||
mockPersistence = jasmine.createSpyObj("persistence", ["persist"]);
|
||||
mockContext = jasmine.createSpyObj('context', [ 'getParent' ]);
|
||||
|
||||
mockActionCapability.getActions.andReturn([{}]);
|
||||
mockActionCapability.getActions.andReturn([mockAction]);
|
||||
mockSwimlane.parent.domainObject.getId.andReturn('a');
|
||||
mockSwimlane.domainObject.getId.andReturn('b');
|
||||
mockSwimlane.children[0].domainObject.getId.andReturn('c');
|
||||
mockOtherObject.getId.andReturn('d');
|
||||
|
||||
|
||||
mockSwimlane.domainObject.getCapability.andCallFake(function (c) {
|
||||
return {
|
||||
action: mockActionCapability,
|
||||
persistence: mockPersistence
|
||||
}[c];
|
||||
});
|
||||
mockOtherObject.getCapability.andReturn(mockActionCapability);
|
||||
mockSwimlane.parent.domainObject.getCapability.andCallFake(function (c) {
|
||||
return {
|
||||
action: mockActionCapability,
|
||||
persistence: mockPersistence
|
||||
}[c];
|
||||
});
|
||||
mockOtherObject.getCapability.andCallFake(function (c) {
|
||||
return {
|
||||
action: mockActionCapability,
|
||||
context: mockContext
|
||||
}[c];
|
||||
});
|
||||
mockContext.getParent.andReturn(mockOtherObject);
|
||||
|
||||
mockSwimlane.domainObject.hasCapability.andReturn(true);
|
||||
|
||||
@ -89,13 +112,17 @@ define(
|
||||
|
||||
it("disallows drop outside of edit mode", function () {
|
||||
// Verify precondition
|
||||
expect(handler.allowDropIn('d')).toBeTruthy();
|
||||
expect(handler.allowDropAfter('d')).toBeTruthy();
|
||||
expect(handler.allowDropIn('d', mockSwimlane.domainObject))
|
||||
.toBeTruthy();
|
||||
expect(handler.allowDropAfter('d', mockSwimlane.domainObject))
|
||||
.toBeTruthy();
|
||||
// Act as if we're not in edit mode
|
||||
mockSwimlane.domainObject.hasCapability.andReturn(false);
|
||||
// Now, they should be disallowed
|
||||
expect(handler.allowDropIn('d')).toBeFalsy();
|
||||
expect(handler.allowDropAfter('d')).toBeFalsy();
|
||||
expect(handler.allowDropIn('d', mockSwimlane.domainObject))
|
||||
.toBeFalsy();
|
||||
expect(handler.allowDropAfter('d', mockSwimlane.domainObject))
|
||||
.toBeFalsy();
|
||||
|
||||
// Verify that editor capability was really checked for
|
||||
expect(mockSwimlane.domainObject.hasCapability)
|
||||
@ -103,8 +130,9 @@ define(
|
||||
});
|
||||
|
||||
it("disallows dropping of parents", function () {
|
||||
expect(handler.allowDropIn('a')).toBeFalsy();
|
||||
expect(handler.allowDropAfter('a')).toBeFalsy();
|
||||
var mockParent = mockSwimlane.parent.domainObject;
|
||||
expect(handler.allowDropIn('a', mockParent)).toBeFalsy();
|
||||
expect(handler.allowDropAfter('a', mockParent)).toBeFalsy();
|
||||
});
|
||||
|
||||
it("does not drop when no highlight state is present", function () {
|
||||
@ -121,7 +149,7 @@ define(
|
||||
it("inserts into when highlighted", function () {
|
||||
var testModel = { composition: [ 'c' ] };
|
||||
mockSwimlane.highlight.andReturn(true);
|
||||
handler.drop('d');
|
||||
handler.drop('d', mockOtherObject);
|
||||
// Should have mutated
|
||||
expect(mockSwimlane.domainObject.useCapability)
|
||||
.toHaveBeenCalledWith("mutation", jasmine.any(Function));
|
||||
@ -133,24 +161,11 @@ define(
|
||||
expect(mockPersistence.persist).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("removes objects before insertion, if provided", function () {
|
||||
var testModel = { composition: [ 'c' ] };
|
||||
mockSwimlane.highlight.andReturn(true);
|
||||
handler.drop('d', mockOtherObject);
|
||||
// Should have invoked a remove action
|
||||
expect(mockActionCapability.perform)
|
||||
.toHaveBeenCalledWith('remove');
|
||||
// Verify that mutator still ran as expected
|
||||
mockSwimlane.domainObject.useCapability.mostRecentCall
|
||||
.args[1](testModel);
|
||||
expect(testModel.composition).toEqual(['c', 'd']);
|
||||
});
|
||||
|
||||
it("inserts after as a peer when highlighted at the bottom", function () {
|
||||
var testModel = { composition: [ 'x', 'b', 'y' ] };
|
||||
mockSwimlane.highlightBottom.andReturn(true);
|
||||
mockSwimlane.expanded = false;
|
||||
handler.drop('d');
|
||||
handler.drop('d', mockOtherObject);
|
||||
// Should have mutated
|
||||
expect(mockSwimlane.parent.domainObject.useCapability)
|
||||
.toHaveBeenCalledWith("mutation", jasmine.any(Function));
|
||||
@ -164,7 +179,7 @@ define(
|
||||
var testModel = { composition: [ 'c' ] };
|
||||
mockSwimlane.highlightBottom.andReturn(true);
|
||||
mockSwimlane.expanded = true;
|
||||
handler.drop('d');
|
||||
handler.drop('d', mockOtherObject);
|
||||
// Should have mutated
|
||||
expect(mockSwimlane.domainObject.useCapability)
|
||||
.toHaveBeenCalledWith("mutation", jasmine.any(Function));
|
||||
@ -179,7 +194,7 @@ define(
|
||||
mockSwimlane.highlightBottom.andReturn(true);
|
||||
mockSwimlane.expanded = true;
|
||||
mockSwimlane.children = [];
|
||||
handler.drop('d');
|
||||
handler.drop('d', mockOtherObject);
|
||||
// Should have mutated
|
||||
expect(mockSwimlane.parent.domainObject.useCapability)
|
||||
.toHaveBeenCalledWith("mutation", jasmine.any(Function));
|
||||
@ -189,6 +204,27 @@ define(
|
||||
expect(testModel.composition).toEqual([ 'x', 'b', 'd', 'y']);
|
||||
});
|
||||
|
||||
it("allows reordering within a parent", function () {
|
||||
var testModel = { composition: [ 'x', 'b', 'y', 'd' ] };
|
||||
mockSwimlane.highlightBottom.andReturn(true);
|
||||
mockSwimlane.expanded = true;
|
||||
mockSwimlane.children = [];
|
||||
mockContext.getParent
|
||||
.andReturn(mockSwimlane.parent.domainObject);
|
||||
handler.drop('d', mockOtherObject);
|
||||
|
||||
waitsFor(function () {
|
||||
return mockSwimlane.parent.domainObject.useCapability
|
||||
.calls.length > 0;
|
||||
});
|
||||
|
||||
runs(function () {
|
||||
mockSwimlane.parent.domainObject.useCapability.mostRecentCall
|
||||
.args[1](testModel);
|
||||
expect(testModel.composition).toEqual([ 'x', 'b', 'd', 'y']);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
);
|
||||
);
|
||||
|
Loading…
Reference in New Issue
Block a user