Merge remote-tracking branch 'origin/master' into table-export-934

Conflicts:
	platform/commonUI/general/res/sass/controls/_buttons.scss
	platform/features/table/res/templates/mct-table.html
This commit is contained in:
Victor Woeltjen 2016-06-15 10:05:20 -07:00
commit 50bd233b0a
32 changed files with 318 additions and 253 deletions

View File

@ -1,6 +1,6 @@
{ {
"name": "openmct", "name": "openmct",
"version": "0.10.2-SNAPSHOT", "version": "0.10.3-SNAPSHOT",
"description": "The Open MCT core platform", "description": "The Open MCT core platform",
"dependencies": { "dependencies": {
"express": "^4.13.1", "express": "^4.13.1",

View File

@ -170,7 +170,7 @@ define([
"navigationService", "navigationService",
"$log" "$log"
], ],
"description": "Edit this object.", "description": "Edit",
"category": "view-control", "category": "view-control",
"glyph": "p" "glyph": "p"
}, },

View File

@ -67,10 +67,17 @@ define(
} }
function onCancel() { function onCancel() {
if (self.domainObject.getModel().persisted !== undefined) {
//Fetch clean model from persistence
return self.persistenceCapability.refresh().then(function (result) { return self.persistenceCapability.refresh().then(function (result) {
self.persistPending = false; self.persistPending = false;
return result; return result;
}); });
} else {
self.persistPending = false;
//Model is undefined in persistence, so return undefined.
return self.$q.when(undefined);
}
} }
if (this.transactionService.isActive()) { if (this.transactionService.isActive()) {

View File

@ -24,12 +24,11 @@ define(
["../../src/actions/CancelAction"], ["../../src/actions/CancelAction"],
function (CancelAction) { function (CancelAction) {
//TODO: Disabled for NEM Beta describe("The Cancel action", function () {
xdescribe("The Cancel action", function () { var mockDomainObject,
var mockLocation, mockParentObject,
mockDomainObject, capabilities = {},
mockEditorCapability, parentCapabilities = {},
mockUrlService,
actionContext, actionContext,
action; action;
@ -42,61 +41,109 @@ define(
} }
beforeEach(function () { beforeEach(function () {
mockLocation = jasmine.createSpyObj(
"$location",
["path"]
);
mockDomainObject = jasmine.createSpyObj( mockDomainObject = jasmine.createSpyObj(
"domainObject", "domainObject",
["getCapability", "hasCapability"] [
"getCapability",
"hasCapability",
"getModel"
]
); );
mockEditorCapability = jasmine.createSpyObj( mockDomainObject.getModel.andReturn({});
mockParentObject = jasmine.createSpyObj(
"parentObject",
[
"getCapability"
]
);
mockParentObject.getCapability.andCallFake(function (name) {
return parentCapabilities[name];
});
capabilities.editor = jasmine.createSpyObj(
"editor", "editor",
["save", "cancel"] ["save", "cancel", "isEditContextRoot"]
); );
mockUrlService = jasmine.createSpyObj( capabilities.action = jasmine.createSpyObj(
"urlService", "actionCapability",
["urlForLocation"] [
"perform"
]
);
capabilities.location = jasmine.createSpyObj(
"locationCapability",
[
"getOriginal"
]
);
capabilities.location.getOriginal.andReturn(mockPromise(mockDomainObject));
capabilities.context = jasmine.createSpyObj(
"contextCapability",
[
"getParent"
]
);
capabilities.context.getParent.andReturn(mockParentObject);
parentCapabilities.action = jasmine.createSpyObj(
"actionCapability",
[
"perform"
]
); );
actionContext = { actionContext = {
domainObject: mockDomainObject domainObject: mockDomainObject
}; };
mockDomainObject.hasCapability.andReturn(true); mockDomainObject.getCapability.andCallFake(function (name) {
mockDomainObject.getCapability.andReturn(mockEditorCapability); return capabilities[name];
mockEditorCapability.cancel.andReturn(mockPromise(true)); });
action = new CancelAction(mockLocation, mockUrlService, actionContext); mockDomainObject.hasCapability.andCallFake(function (name) {
return !!capabilities[name];
});
capabilities.editor.cancel.andReturn(mockPromise(true));
action = new CancelAction(actionContext);
}); });
it("only applies to domain object with an editor capability", function () { it("only applies to domain object that is being edited", function () {
capabilities.editor.isEditContextRoot.andReturn(true);
expect(CancelAction.appliesTo(actionContext)).toBeTruthy(); expect(CancelAction.appliesTo(actionContext)).toBeTruthy();
expect(mockDomainObject.hasCapability).toHaveBeenCalledWith("editor"); expect(mockDomainObject.hasCapability).toHaveBeenCalledWith("editor");
capabilities.editor.isEditContextRoot.andReturn(false);
expect(CancelAction.appliesTo(actionContext)).toBeFalsy();
mockDomainObject.hasCapability.andReturn(false); mockDomainObject.hasCapability.andReturn(false);
mockDomainObject.getCapability.andReturn(undefined);
expect(CancelAction.appliesTo(actionContext)).toBeFalsy(); expect(CancelAction.appliesTo(actionContext)).toBeFalsy();
}); });
it("invokes the editor capability's save functionality when performed", function () { it("invokes the editor capability's cancel functionality when" +
// Verify precondition " performed", function () {
expect(mockEditorCapability.cancel).not.toHaveBeenCalled();
action.perform(); action.perform();
// Should have called cancel // Should have called cancel
expect(mockEditorCapability.cancel).toHaveBeenCalled(); expect(capabilities.editor.cancel).toHaveBeenCalled();
// Definitely shouldn't call save! // Definitely shouldn't call save!
expect(mockEditorCapability.save).not.toHaveBeenCalled(); expect(capabilities.editor.save).not.toHaveBeenCalled();
}); });
it("returns to browse when performed", function () { it("navigates to object if existing", function () {
mockDomainObject.getModel.andReturn({persisted: 1});
action.perform(); action.perform();
expect(mockLocation.path).toHaveBeenCalledWith( expect(capabilities.action.perform).toHaveBeenCalledWith("navigate");
mockUrlService.urlForLocation("browse", mockDomainObject) });
);
it("navigates to parent if new", function () {
mockDomainObject.getModel.andReturn({persisted: undefined});
action.perform();
expect(parentCapabilities.action.perform).toHaveBeenCalledWith("navigate");
}); });
}); });
} }

View File

@ -57,6 +57,15 @@ define(
); );
mockPersistence.persist.andReturn(fastPromise()); mockPersistence.persist.andReturn(fastPromise());
mockPersistence.refresh.andReturn(fastPromise()); mockPersistence.refresh.andReturn(fastPromise());
mockDomainObject = jasmine.createSpyObj(
"domainObject",
[
"getModel"
]
);
mockDomainObject.getModel.andReturn({persisted: 1});
capability = new TransactionalPersistenceCapability(mockQ, mockTransactionService, mockPersistence, mockDomainObject); capability = new TransactionalPersistenceCapability(mockQ, mockTransactionService, mockPersistence, mockDomainObject);
}); });
@ -78,6 +87,20 @@ define(
expect(mockPersistence.refresh).toHaveBeenCalled(); expect(mockPersistence.refresh).toHaveBeenCalled();
}); });
it("if transaction is active, cancel call is queued that refreshes model when appropriate", function () {
mockTransactionService.isActive.andReturn(true);
capability.persist();
expect(mockTransactionService.addToTransaction).toHaveBeenCalled();
mockDomainObject.getModel.andReturn({});
mockTransactionService.addToTransaction.mostRecentCall.args[1]();
expect(mockPersistence.refresh).not.toHaveBeenCalled();
mockDomainObject.getModel.andReturn({persisted: 1});
mockTransactionService.addToTransaction.mostRecentCall.args[1]();
expect(mockPersistence.refresh).toHaveBeenCalled();
});
it("persist call is only added to transaction once", function () { it("persist call is only added to transaction once", function () {
mockTransactionService.isActive.andReturn(true); mockTransactionService.isActive.andReturn(true);
capability.persist(); capability.persist();

View File

@ -124,6 +124,8 @@ $dirImgs: $dirCommonRes + 'images/';
/************************** TIMINGS */ /************************** TIMINGS */
$controlFadeMs: 100ms; $controlFadeMs: 100ms;
$browseToEditAnimMs: 400ms;
$editBorderPulseMs: 500ms;
/************************** LIMITS */ /************************** LIMITS */
$glyphLimit: '\e603'; $glyphLimit: '\e603';

View File

@ -39,15 +39,20 @@
@include pulse($animName: pulse-subtle, $dur: 500ms, $opacity0: 0.7); @include pulse($animName: pulse-subtle, $dur: 500ms, $opacity0: 0.7);
} }
@mixin pulseBorder($c: red, $dur: 500ms, $iteration: infinite, $delay: 0s, $opacity0: 0, $opacity100: 1) { @mixin animTo($animName, $propName, $propValStart, $propValEnd, $dur: 500ms, $delay: 0) {
@include keyframes(pulseBorder) { @include keyframes($animName) {
0% { border-color: rgba($c, $opacity0); } from { #{propName}: $propValStart; }
100% { border-color: rgba($c, $opacity100); } to { #{$propName}: $propValEnd; }
} }
@include animation-name(pulseBorder); @include animToParams($animName, $dur: 500ms, $delay: 0)
}
@mixin animToParams($animName, $dur: 500ms, $delay: 0) {
@include animation-name($animName);
@include animation-duration($dur); @include animation-duration($dur);
@include animation-direction(alternate);
@include animation-iteration-count($iteration);
@include animation-timing-function(ease);
@include animation-delay($delay); @include animation-delay($delay);
@include animation-fill-mode(both);
@include animation-direction(normal);
@include animation-iteration-count(1);
@include animation-timing-function(ease-in-out);
} }

View File

@ -36,15 +36,7 @@ $pad: $interiorMargin * $baseRatio;
padding: 0 $pad; padding: 0 $pad;
font-size: 0.7rem; font-size: 0.7rem;
vertical-align: top; vertical-align: top;
@include btnSubtle($colorBtnBg, $colorBtnBgHov, $colorBtnFg, $colorBtnIcon);
.icon {
font-size: 0.8rem;
color: $colorKey;
}
.title-label {
vertical-align: top;
}
&.lg { &.lg {
font-size: 1rem; font-size: 1rem;
@ -58,19 +50,13 @@ $pad: $interiorMargin * $baseRatio;
padding: 0 ($pad / $baseRatio) / 2; padding: 0 ($pad / $baseRatio) / 2;
} }
&.major { &.major,
&.key-edit {
$bg: $colorBtnMajorBg; $bg: $colorBtnMajorBg;
$hc: lighten($bg, 10%); $hc: lighten($bg, 10%);
@include btnSubtle($bg, $hc, $colorBtnMajorFg, $colorBtnMajorFg); @include btnSubtle($bg, $hc, $colorBtnMajorFg, $colorBtnMajorFg);
} }
&:not(.major) {
// bg, bgHov, fg, ic
@include btnSubtle($colorBtnBg, $colorBtnBgHov, $colorBtnFg, $colorBtnIcon);
}
&.pause-play {
}
&.t-save:before { &.t-save:before {
content:'\e612'; content:'\e612';
font-family: symbolsfont; font-family: symbolsfont;
@ -117,6 +103,15 @@ $pad: $interiorMargin * $baseRatio;
content: '\e623'; content: '\e623';
} }
} }
.icon {
font-size: 0.8rem;
color: $colorKey;
}
.title-label {
vertical-align: top;
}
} }
.s-icon-btn { .s-icon-btn {
@ -283,4 +278,3 @@ body.desktop .mini-tab-icon {
color: $colorPausedBg !important; color: $colorPausedBg !important;
} }
} }

View File

@ -237,30 +237,10 @@ body.desktop .pane .mini-tab-icon.toggle-pane {
top: $ueTopBarH + $interiorMarginLg; top: $ueTopBarH + $interiorMarginLg;
} }
.l-object-wrapper {
@extend .abs;
.object-holder-main {
@extend .abs;
}
.l-edit-controls {
//@include trans-prop-nice((opacity, height), 0.25s);
border-bottom: 1px solid $colorInteriorBorder;
line-height: $ueEditToolBarH;
height: 0px;
opacity: 0;
.tool-bar {
right: $interiorMargin;
}
}
}
.l-object-wrapper-inner { .l-object-wrapper-inner {
@include trans-prop-nice-resize(0.25s); @include trans-prop-nice-resize(0.25s);
} }
.object-browse-bar .s-btn, .object-browse-bar .s-btn,
.top-bar .buttons-main .s-btn, .top-bar .buttons-main .s-btn,
.top-bar .s-menu-btn, .top-bar .s-menu-btn,
@ -377,19 +357,50 @@ body.desktop {
.s-status-editing { .s-status-editing {
.l-object-wrapper { .l-object-wrapper {
@include pulseBorder($colorEditAreaFg, $dur: 1s, $opacity0: 0.3); $t2Dur: $browseToEditAnimMs;
border-radius: $controlCr; $t1Dur: $t2Dur / 2;
$pulseDur: $editBorderPulseMs;
$bC0: rgba($colorEditAreaFg, 0.5);
$bC100: rgba($colorEditAreaFg, 1);
background-color: $colorEditAreaBg; background-color: $colorEditAreaBg;
border-color: $colorEditAreaFg; border-radius: $controlCr;
border-width: 2px; border: 1px dotted $bC0;
border-style: dotted;
.l-object-wrapper-inner { // Transition 1
@include absPosDefault(3px, hidden); @include keyframes(wrapperIn) {
from { border: 0px dotted transparent; padding: 0; }
to { border: 1px dotted $bC0; padding: 5px; }
} }
// Do last
@include keyframes(pulseNew) {
from { border-color: $bC0; }
to { border-color: $bC100; }
}
@include animation-name(wrapperIn, pulseNew);
@include animation-duration($t1Dur, $pulseDur);
@include animation-delay(0s, $t1Dur + $t2Dur);
@include animation-direction(normal, alternate);
@include animation-fill-mode(both, none);
@include animation-iteration-count(1, infinite);
@include animation-timing-function(ease-in-out, linear);
.l-edit-controls { .l-edit-controls {
height: $ueEditToolBarH + $interiorMargin; height: 0;
margin-bottom: $interiorMargin; border-bottom: 1px solid $colorInteriorBorder;
opacity: 1; overflow: hidden;
// Transition 2: reveal edit controls
@include keyframes(editIn) {
from { border-bottom: 0px solid transparent; height: 0; margin-bottom: 0; }
to { border-bottom: 1px solid $colorInteriorBorder; height: $ueEditToolBarH + $interiorMargin; margin-bottom: $interiorMargin; }
}
@include animToParams(editIn, $dur: $t2Dur, $delay: $t1Dur);
.tool-bar {
right: $interiorMargin;
}
} }
} }
} }

View File

@ -29,10 +29,12 @@
!structure.validate(ngModel[field])), !structure.validate(ngModel[field])),
'picker-icon': structure.format === 'utc' || !structure.format 'picker-icon': structure.format === 'utc' || !structure.format
}"> }">
</input><a class="ui-symbol icon icon-calendar" </input>
ng-if="structure.format === 'utc' || !structure.format" <a class="ui-symbol icon icon-calendar"
ng-click="picker.active = !picker.active"> ng-if="!picker.active && (structure.format === 'utc' || !structure.format)"
</a> ng-click="picker.active = !picker.active"></a>
<!-- If picker active show icon with no onclick to prevent double registration of clicks -->
<a class="ui-symbol icon icon-calendar" ng-if="picker.active"></a>
<mct-popup ng-if="picker.active"> <mct-popup ng-if="picker.active">
<div mct-click-elsewhere="picker.active = false"> <div mct-click-elsewhere="picker.active = false">
<mct-control key="'datetime-picker'" <mct-control key="'datetime-picker'"

View File

@ -72,6 +72,17 @@ define(
if ($scope.ngBlur) { if ($scope.ngBlur) {
$scope.ngBlur(); $scope.ngBlur();
} }
// If picker is active, dismiss it when valid value has been selected
// This 'if' is to avoid unnecessary validation if picker is not active
if ($scope.picker.active) {
if ($scope.structure.validate && $scope.structure.validate($scope.ngModel[$scope.field])) {
$scope.picker.active = false;
} else if (!$scope.structure.validate) {
//If picker visible, but no validation function, hide picker
$scope.picker.active = false;
}
}
} }
} }
@ -93,7 +104,6 @@ define(
$scope.$watch('ngModel[field]', updateFromModel); $scope.$watch('ngModel[field]', updateFromModel);
$scope.$watch('pickerModel.value', updateFromPicker); $scope.$watch('pickerModel.value', updateFromPicker);
$scope.$watch('textValue', updateFromView); $scope.$watch('textValue', updateFromView);
} }
return DateTimeFieldController; return DateTimeFieldController;

View File

@ -51,7 +51,9 @@ define(
yMax = yMin + rect.height; yMax = yMin + rect.height;
if (x < xMin || x > xMax || y < yMin || y > yMax) { if (x < xMin || x > xMax || y < yMin || y > yMax) {
scope.$apply(function () {
scope.$eval(attrs.mctClickElsewhere); scope.$eval(attrs.mctClickElsewhere);
});
} }
} }

View File

@ -104,6 +104,8 @@ define(
}); });
it("triggers an evaluation of its related Angular expression", function () { it("triggers an evaluation of its related Angular expression", function () {
expect(mockScope.$apply).toHaveBeenCalled();
mockScope.$apply.mostRecentCall.args[0]();
expect(mockScope.$eval) expect(mockScope.$eval)
.toHaveBeenCalledWith(testAttrs.mctClickElsewhere); .toHaveBeenCalledWith(testAttrs.mctClickElsewhere);
}); });

View File

@ -14,7 +14,7 @@ $colorAHov: #fff;
$contrastRatioPercent: 7%; $contrastRatioPercent: 7%;
$hoverRatioPercent: 10%; $hoverRatioPercent: 10%;
$basicCr: 3px; $basicCr: 3px;
$controlCr: 3px; $controlCr: 2px;
$smallCr: 2px; $smallCr: 2px;
// Buttons and Controls // Buttons and Controls

View File

@ -82,16 +82,6 @@ define(
expect(result.a.getModel()).toEqual(model); expect(result.a.getModel()).toEqual(model);
}); });
//TODO: Disabled for NEM Beta
xit("provides a new, fully constituted domain object for a" +
" provided model", function () {
var model = { someKey: "some value"},
result;
result = provider.newObject("a", model);
expect(result.getId()).toEqual("a");
expect(result.getModel()).toEqual(model);
});
}); });
} }
); );

View File

@ -81,6 +81,7 @@ define(
if (phase.toLowerCase() === 'preparing' && !this.dialog) { if (phase.toLowerCase() === 'preparing' && !this.dialog) {
this.dialog = this.dialogService.showBlockingMessage({ this.dialog = this.dialogService.showBlockingMessage({
title: "Preparing to copy objects", title: "Preparing to copy objects",
hint: "Do not navigate away from this page or close this browser tab while this message is displayed.",
unknownProgress: true, unknownProgress: true,
severity: "info" severity: "info"
}); });

View File

@ -24,10 +24,7 @@ define(
[], [],
function () { function () {
var DISALLOWED_ACTIONS = [ var DISALLOWED_ACTIONS = ["move"];
"move",
"copy"
];
/** /**
* This policy prevents performing move/copy/link actions across * This policy prevents performing move/copy/link actions across

View File

@ -70,10 +70,9 @@ define(
policy = new CrossSpacePolicy(); policy = new CrossSpacePolicy();
}); });
['move', 'copy'].forEach(function (key) { describe("for move actions", function () {
describe("for " + key + " actions", function () {
beforeEach(function () { beforeEach(function () {
testActionMetadata.key = key; testActionMetadata.key = 'move';
}); });
it("allows same-space changes", function () { it("allows same-space changes", function () {
@ -92,7 +91,6 @@ define(
})).toBe(true); })).toBe(true);
}); });
}); });
});
describe("for other actions", function () { describe("for other actions", function () {
beforeEach(function () { beforeEach(function () {

View File

@ -42,7 +42,7 @@ define(
'parent' 'parent'
]; ];
xdescribe("ConductorRepresenter", function () { describe("ConductorRepresenter", function () {
var mockThrottle, var mockThrottle,
mockConductorService, mockConductorService,
mockCompile, mockCompile,

View File

@ -3,7 +3,7 @@
title="Export This View's Data"> title="Export This View's Data">
Export Export
</a> </a>
<div class="l-view-section scrolling" style="overflow: auto;"> <div class="l-view-section scrolling" style="overflow: auto;" mct-resize="resize()">
<table class="sizing-table"> <table class="sizing-table">
<tbody> <tbody>
<tr> <tr>

View File

@ -86,6 +86,12 @@ define(
*/ */
$scope.$on('add:row', this.addRow.bind(this)); $scope.$on('add:row', this.addRow.bind(this));
$scope.$on('remove:row', this.removeRow.bind(this)); $scope.$on('remove:row', this.removeRow.bind(this));
/*
* Listen for resize events to trigger recalculation of table width
*/
$scope.resize = this.setElementSizes.bind(this);
} }
/** /**

View File

@ -30,23 +30,21 @@ define(
var TEST_DOMAIN_VALUE = "some formatted domain value"; var TEST_DOMAIN_VALUE = "some formatted domain value";
describe("A domain column", function () { describe("A domain column", function () {
var mockDataSet, var mockDatum,
testMetadata, testMetadata,
mockFormatter, mockFormatter,
column; column;
beforeEach(function () { beforeEach(function () {
mockDataSet = jasmine.createSpyObj(
"data",
["getDomainValue"]
);
mockFormatter = jasmine.createSpyObj( mockFormatter = jasmine.createSpyObj(
"formatter", "formatter",
["formatDomainValue", "formatRangeValue"] ["formatDomainValue", "formatRangeValue"]
); );
testMetadata = { testMetadata = {
key: "testKey", key: "testKey",
name: "Test Name" name: "Test Name",
format: "Test Format"
}; };
mockFormatter.formatDomainValue.andReturn(TEST_DOMAIN_VALUE); mockFormatter.formatDomainValue.andReturn(TEST_DOMAIN_VALUE);
@ -57,24 +55,24 @@ define(
expect(column.getTitle()).toEqual("Test Name"); expect(column.getTitle()).toEqual("Test Name");
}); });
xit("looks up data from a data set", function () { describe("when given a datum", function () {
column.getValue(undefined, mockDataSet, 42); beforeEach(function () {
expect(mockDataSet.getDomainValue) mockDatum = {
.toHaveBeenCalledWith(42, "testKey"); testKey: "testKeyValue"
};
}); });
xit("formats domain values as time", function () { it("looks up data from the given datum", function () {
mockDataSet.getDomainValue.andReturn(402513731000); expect(column.getValue(undefined, mockDatum))
.toEqual({ text: TEST_DOMAIN_VALUE });
});
// Should have just given the value the formatter gave it("uses formatter to format domain values as requested", function () {
expect(column.getValue(undefined, mockDataSet, 42).text) column.getValue(undefined, mockDatum);
.toEqual(TEST_DOMAIN_VALUE);
// Make sure that service interactions were as expected
expect(mockFormatter.formatDomainValue) expect(mockFormatter.formatDomainValue)
.toHaveBeenCalledWith(402513731000); .toHaveBeenCalledWith("testKeyValue", "Test Format");
expect(mockFormatter.formatRangeValue) });
.not.toHaveBeenCalled();
}); });
}); });

View File

@ -472,6 +472,7 @@ define([
"implementation": TimelineZoomController, "implementation": TimelineZoomController,
"depends": [ "depends": [
"$scope", "$scope",
"$window",
"TIMELINE_ZOOM_CONFIGURATION" "TIMELINE_ZOOM_CONFIGURATION"
] ]
}, },

View File

@ -20,7 +20,7 @@
at runtime from the About dialog for additional information. at runtime from the About dialog for additional information.
--> -->
<div class="t-timeline-gantt l-timeline-gantt s-timeline-gantt" <div class="t-timeline-gantt l-timeline-gantt s-timeline-gantt"
ng-class="{ sm: gantt.width(timespan, parameters.scroll, parameters.toPixels) < 25 }" ng-class="timespan ? { sm: gantt.width(timespan, parameters.scroll, parameters.toPixels) < 25 } : {}"
title="{{model.name}}" title="{{model.name}}"
ng-controller="TimelineGanttController as gantt" ng-controller="TimelineGanttController as gantt"
ng-style="timespan ? { ng-style="timespan ? {

View File

@ -128,7 +128,7 @@
<div style="overflow: hidden; position: absolute; left: 0; top: 0; right: 0; height: 30px;" mct-scroll-x="scroll.x"> <div style="overflow: hidden; position: absolute; left: 0; top: 0; right: 0; height: 30px;" mct-scroll-x="scroll.x">
<mct-include key="'timeline-ticks'" <mct-include key="'timeline-ticks'"
parameters="{ parameters="{
fullWidth: timelineController.width(zoomController), fullWidth: zoomController.width(timelineController.end()),
start: scroll.x, start: scroll.x,
width: scroll.width, width: scroll.width,
step: zoomController.toPixels(zoomController.zoom()), step: zoomController.toPixels(zoomController.zoom()),
@ -141,7 +141,7 @@
mct-scroll-x="scroll.x" mct-scroll-x="scroll.x"
mct-scroll-y="scroll.y"> mct-scroll-y="scroll.y">
<div class="l-width-control" <div class="l-width-control"
ng-style="{ width: timelineController.width(zoomController) + 'px' }"> ng-style="{ width: zoomController.width(timelineController.end()) + 'px' }">
<div class="t-swimlane s-swimlane l-swimlane" <div class="t-swimlane s-swimlane l-swimlane"
ng-repeat="swimlane in timelineController.swimlanes()" ng-repeat="swimlane in timelineController.swimlanes()"
ng-class="{ ng-class="{
@ -197,7 +197,7 @@
<div mct-scroll-x="scroll.x" <div mct-scroll-x="scroll.x"
class="t-pane-r-scroll-h-control l-scroll-control s-scroll-control"> class="t-pane-r-scroll-h-control l-scroll-control s-scroll-control">
<div class="l-width-control" <div class="l-width-control"
ng-style="{ width: timelineController.width(zoomController) + 'px' }"> ng-style="{ width: zoomController.width(timelineController.end()) + 'px' }">
</div> </div>
</div> </div>
</div> </div>

View File

@ -79,15 +79,6 @@ define(
graphPopulator.populate(swimlanePopulator.get()); graphPopulator.populate(swimlanePopulator.get());
} }
// Get pixel width for right pane, using zoom controller
function width(zoomController) {
var start = swimlanePopulator.start(),
end = swimlanePopulator.end();
return zoomController.toPixels(zoomController.duration(
Math.max(end - start, MINIMUM_DURATION)
));
}
// Refresh resource graphs // Refresh resource graphs
function refresh() { function refresh() {
if (graphPopulator) { if (graphPopulator) {
@ -121,10 +112,10 @@ define(
// Expose active set of swimlanes // Expose active set of swimlanes
return { return {
/** /**
* Get the width, in pixels, of the timeline area * Get the end of the displayed timeline, in milliseconds.
* @returns {number} width, in pixels * @returns {number} the end of the displayed timeline
*/ */
width: width, end: swimlanePopulator.end.bind(swimlanePopulator),
/** /**
* Get the swimlanes which should currently be displayed. * Get the swimlanes which should currently be displayed.
* @returns {TimelineSwimlane[]} the swimlanes * @returns {TimelineSwimlane[]} the swimlanes

View File

@ -22,27 +22,17 @@
define( define(
[], [],
function () { function () {
var PADDING = 0.25;
/** /**
* Controls the pan-zoom state of a timeline view. * Controls the pan-zoom state of a timeline view.
* @constructor * @constructor
*/ */
function TimelineZoomController($scope, ZOOM_CONFIGURATION) { function TimelineZoomController($scope, $window, ZOOM_CONFIGURATION) {
// Prefer to start with the middle index // Prefer to start with the middle index
var zoomLevels = ZOOM_CONFIGURATION.levels || [1000], var zoomLevels = ZOOM_CONFIGURATION.levels || [1000],
zoomIndex = Math.floor(zoomLevels.length / 2), zoomIndex = Math.floor(zoomLevels.length / 2),
tickWidth = ZOOM_CONFIGURATION.width || 200, tickWidth = ZOOM_CONFIGURATION.width || 200;
bounds = { x: 0, width: tickWidth },
duration = 86400000; // Default duration in view
// Round a duration to a larger value, to ensure space for editing
function roundDuration(value) {
// Ensure there's always an extra day or so
var tickCount = bounds.width / tickWidth,
sz = zoomLevels[zoomLevels.length - 1] * tickCount;
value *= 1.25; // Add 25% padding to start
return Math.ceil(value / sz) * sz;
}
function toMillis(pixels) { function toMillis(pixels) {
return (pixels / tickWidth) * zoomLevels[zoomIndex]; return (pixels / tickWidth) * zoomLevels[zoomIndex];
@ -63,14 +53,21 @@ define(
} }
} }
function setScroll(x) {
$window.requestAnimationFrame(function () {
$scope.scroll.x = x;
$scope.$apply();
});
}
function initializeZoomFromTimespan(timespan) { function initializeZoomFromTimespan(timespan) {
var timelineDuration = timespan.getDuration(); var timelineDuration = timespan.getDuration();
zoomIndex = 0; zoomIndex = 0;
while (toMillis(bounds.width) < timelineDuration && while (toMillis($scope.scroll.width) < timelineDuration &&
zoomIndex < zoomLevels.length - 1) { zoomIndex < zoomLevels.length - 1) {
zoomIndex += 1; zoomIndex += 1;
} }
bounds.x = toPixels(timespan.getStart()); setScroll(toPixels(timespan.getStart()));
} }
function initializeZoom() { function initializeZoom() {
@ -80,9 +77,6 @@ define(
} }
} }
$scope.$watch("scroll", function (scroll) {
bounds = scroll;
});
$scope.$watch("domainObject", initializeZoom); $scope.$watch("domainObject", initializeZoom);
return { return {
@ -100,9 +94,10 @@ define(
zoom: function (amount) { zoom: function (amount) {
// Update the zoom level if called with an argument // Update the zoom level if called with an argument
if (arguments.length > 0 && !isNaN(amount)) { if (arguments.length > 0 && !isNaN(amount)) {
var bounds = $scope.scroll;
var center = this.toMillis(bounds.x + bounds.width / 2); var center = this.toMillis(bounds.x + bounds.width / 2);
setZoomLevel(zoomIndex + amount); setZoomLevel(zoomIndex + amount);
bounds.x = this.toPixels(center) - bounds.width / 2; setScroll(this.toPixels(center) - bounds.width / 2);
} }
return zoomLevels[zoomIndex]; return zoomLevels[zoomIndex];
}, },
@ -124,16 +119,14 @@ define(
*/ */
toMillis: toMillis, toMillis: toMillis,
/** /**
* Get or set the current displayed duration. If used as a * Get the pixel width necessary to fit the specified
* setter, this will typically be rounded up to ensure extra * timestamp, expressed as an offset in milliseconds from
* space is available at the right. * the start of the timeline.
* @returns {number} duration, in milliseconds * @param {number} timestamp the time to display
*/ */
duration: function (value) { width: function (timestamp) {
if (arguments.length > 0) { var pixels = Math.ceil(toPixels(timestamp * (1 + PADDING)));
duration = roundDuration(value); return Math.max($scope.scroll.width, pixels);
}
return duration;
} }
}; };
} }

View File

@ -214,23 +214,6 @@ define(
}); });
it("reports full scrollable width using zoom controller", function () {
var mockZoom = jasmine.createSpyObj('zoom', ['toPixels', 'duration']);
mockZoom.toPixels.andReturn(54321);
mockZoom.duration.andReturn(12345);
// Initially populate
fireWatch('domainObject', mockDomainObject);
expect(controller.width(mockZoom)).toEqual(54321);
// Verify interactions; we took zoom's duration for our start/end,
// and converted it to pixels.
// First, check that we used the start/end (from above)
expect(mockZoom.duration).toHaveBeenCalledWith(12321 - 42);
// Next, verify that the result was passed to toPixels
expect(mockZoom.toPixels).toHaveBeenCalledWith(12345);
});
it("provides drag handles", function () { it("provides drag handles", function () {
// TimelineDragPopulator et al are tested for these, // TimelineDragPopulator et al are tested for these,
// so just verify that handles are indeed exposed. // so just verify that handles are indeed exposed.

View File

@ -28,6 +28,7 @@ define(
describe("The timeline zoom state controller", function () { describe("The timeline zoom state controller", function () {
var testConfiguration, var testConfiguration,
mockScope, mockScope,
mockWindow,
controller; controller;
beforeEach(function () { beforeEach(function () {
@ -35,10 +36,16 @@ define(
levels: [1000, 2000, 3500], levels: [1000, 2000, 3500],
width: 12321 width: 12321
}; };
mockScope = jasmine.createSpyObj("$scope", ['$watch']); mockScope =
jasmine.createSpyObj("$scope", ['$watch', '$apply']);
mockScope.commit = jasmine.createSpy('commit'); mockScope.commit = jasmine.createSpy('commit');
mockScope.scroll = { x: 0, width: 1000 };
mockWindow = {
requestAnimationFrame: jasmine.createSpy('raf')
};
controller = new TimelineZoomController( controller = new TimelineZoomController(
mockScope, mockScope,
mockWindow,
testConfiguration testConfiguration
); );
}); });
@ -47,12 +54,6 @@ define(
expect(controller.zoom()).toEqual(2000); expect(controller.zoom()).toEqual(2000);
}); });
it("allows duration to be changed", function () {
var initial = controller.duration();
controller.duration(initial * 3.33);
expect(controller.duration() > initial).toBeTruthy();
});
it("handles time-to-pixel conversions", function () { it("handles time-to-pixel conversions", function () {
var zoomLevel = controller.zoom(); var zoomLevel = controller.zoom();
expect(controller.toPixels(zoomLevel)).toEqual(12321); expect(controller.toPixels(zoomLevel)).toEqual(12321);
@ -70,11 +71,6 @@ define(
expect(controller.zoom()).toEqual(3500); expect(controller.zoom()).toEqual(3500);
}); });
it("observes scroll bounds", function () {
expect(mockScope.$watch)
.toHaveBeenCalledWith("scroll", jasmine.any(Function));
});
describe("when watches have fired", function () { describe("when watches have fired", function () {
var mockDomainObject, var mockDomainObject,
mockPromise, mockPromise,
@ -115,6 +111,10 @@ define(
mockScope.$watch.calls.forEach(function (call) { mockScope.$watch.calls.forEach(function (call) {
call.args[1](mockScope[call.args[0]]); call.args[1](mockScope[call.args[0]]);
}); });
mockWindow.requestAnimationFrame.calls.forEach(function (call) {
call.args[0]();
});
}); });
it("zooms to fit the timeline", function () { it("zooms to fit the timeline", function () {
@ -125,6 +125,27 @@ define(
expect(Math.round(controller.toMillis(x2))) expect(Math.round(controller.toMillis(x2)))
.toBeGreaterThan(testEnd); .toBeGreaterThan(testEnd);
}); });
it("provides a width which is not less than scroll area width", function () {
var testPixel = mockScope.scroll.width / 4,
testMillis = controller.toMillis(testPixel);
expect(controller.width(testMillis))
.not.toBeLessThan(mockScope.scroll.width);
});
it("provides a width with some margin past timestamp", function () {
var testPixel = mockScope.scroll.width * 4,
testMillis = controller.toMillis(testPixel);
expect(controller.width(testMillis))
.toBeGreaterThan(controller.toPixels(testMillis));
});
it("provides a width which does not greatly exceed timestamp", function () {
var testPixel = mockScope.scroll.width * 4,
testMillis = controller.toMillis(testPixel);
expect(controller.width(testMillis))
.toBeLessThan(controller.toPixels(testMillis * 2));
});
}); });
}); });

View File

@ -61,8 +61,7 @@ define(
{ {
x: event.pageX - rect.left, x: event.pageX - rect.left,
y: event.pageY - rect.top y: event.pageY - rect.top
}, }
domainObject
); );
} }
} }

View File

@ -34,8 +34,7 @@ define(
TEST_ID = "test-id", TEST_ID = "test-id",
DROP_ID = "drop-id"; DROP_ID = "drop-id";
//TODO: Disabled for NEM Beta describe("The drop gesture", function () {
xdescribe("The drop gesture", function () {
var mockDndService, var mockDndService,
mockQ, mockQ,
mockElement, mockElement,
@ -144,23 +143,6 @@ define(
expect(mockCompose.perform).toHaveBeenCalled(); expect(mockCompose.perform).toHaveBeenCalled();
}); });
it("does not invoke compose on drop in browse mode for non-folders", function () {
// Set the mockDomainObject to not have the editor capability
mockDomainObject.hasCapability.andReturn(false);
// Set the mockDomainObject to not have a type of folder
mockDomainObject.getModel.andReturn({type: 'notAFolder'});
callbacks.dragover(mockEvent);
expect(mockAction.getActions).toHaveBeenCalledWith({
key: 'compose',
selectedObject: mockDraggedObject
});
callbacks.drop(mockEvent);
expect(mockCompose.perform).not.toHaveBeenCalled();
});
it("invokes compose on drop in browse mode for folders", function () { it("invokes compose on drop in browse mode for folders", function () {
// Set the mockDomainObject to not have the editor capability // Set the mockDomainObject to not have the editor capability
mockDomainObject.hasCapability.andReturn(false); mockDomainObject.hasCapability.andReturn(false);

View File

@ -34,8 +34,8 @@ var EditItem = (function () {
EditItem.prototype.EditButton = function () { EditItem.prototype.EditButton = function () {
return element.all(by.css('[ng-click="parameters.action.perform()"]')).filter(function (arg) { return element.all(by.css('[ng-click="parameters.action.perform()"]')).filter(function (arg) {
return arg.getAttribute("title").then(function (title){ return arg.getAttribute("title").then(function (title){
//expect(title).toEqual("Edit this object."); //expect(title).toEqual("Edit");
return title == 'Edit this object.'; return title == 'Edit';
}) })
}); });
}; };