Compare commits

...

9 Commits

Author SHA1 Message Date
Deep Tailor
568f89baf2 prevent resizing on context click 2020-12-07 16:51:48 -08:00
Jamie V
d80c0eef8e [Actions] Duplicate Action bug fixes (#3578)
* WIP: refactoring legacy dulicate action

* WIP: debugging duplicate duplicates...

* WIP: fixed duplicate duplicates issue

* added unit tests

* removing old legacy copyaction and renaming duplicate action

* removing fdescribe

* trying to see if a done callback fixes testing issues

* fixed tests

* testing autoflow tests on server

* tweaked autoflow tests to stop failing

* minor updates for new 3 dot menu

* WIP bug fixing

* WIP debugging

* WIP more debuggin

* WIP using parent namespace for all duped objs

* WIP

* WIP
;
;

* cleaning up debugging code

* fixed linting issues

Co-authored-by: Deep Tailor <deep.j.tailor@nasa.gov>
2020-12-07 10:15:41 -08:00
Nikhil
55829dcf05 [Conductor] Remove 24 hour default limit in time conductor (#3512) 2020-12-04 10:34:19 -08:00
Deep Tailor
d78956327c Action API unit tests (#3527)
* add tests

* add tests for ActionCollection

* add tests for getVisibleActions and getStatusBarActions

Co-authored-by: Shefali Joshi <simplyrender@gmail.com>
2020-12-02 12:53:34 -08:00
Nikhil
4a0728a55b Navigating to a Notebook snapshot not working #3538 (#3569) 2020-12-02 12:40:35 -08:00
Deep Tailor
2e1d57aa8c only update selection if selectable has changed (#3573) 2020-12-02 12:00:30 -08:00
Nikhil
1c2b0678be [Notebook] snapshots on plots are empty #3566 (#3567) 2020-12-01 14:14:59 -08:00
Deep Tailor
6f810add43 replace dots with underscores in save as filenames (#3565) 2020-12-01 11:18:13 -08:00
Deep Tailor
12727adb16 fix export marked data as csv (#3563) 2020-12-01 09:44:17 -08:00
12 changed files with 321 additions and 40 deletions

View File

@@ -131,10 +131,10 @@
}
],
// maximum recent bounds to retain in conductor history
records: 10,
records: 10
// maximum duration between start and end bounds
// for utc-based time systems this is in milliseconds
limit: ONE_DAY
// limit: ONE_DAY
},
{
name: "Realtime",

View File

@@ -0,0 +1,225 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2020, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT 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 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.
*****************************************************************************/
import ActionCollection from './ActionCollection';
import { createOpenMct, resetApplicationState } from '../../utils/testing';
describe('The ActionCollection', () => {
let openmct;
let actionCollection;
let mockApplicableActions;
let mockObjectPath;
let mockView;
beforeEach(() => {
openmct = createOpenMct();
mockObjectPath = [
{
name: 'mock folder',
type: 'fake-folder',
identifier: {
key: 'mock-folder',
namespace: ''
}
},
{
name: 'mock parent folder',
type: 'fake-folder',
identifier: {
key: 'mock-parent-folder',
namespace: ''
}
}
];
mockView = {
getViewContext: () => {
return {
onlyAppliesToTestCase: true
};
}
};
mockApplicableActions = {
'test-action-object-path': {
name: 'Test Action Object Path',
key: 'test-action-object-path',
cssClass: 'test-action-object-path',
description: 'This is a test action for object path',
group: 'action',
priority: 9,
appliesTo: (objectPath) => {
if (objectPath.length) {
return objectPath[0].type === 'fake-folder';
}
return false;
},
invoke: () => {
}
},
'test-action-view': {
name: 'Test Action View',
key: 'test-action-view',
cssClass: 'test-action-view',
description: 'This is a test action for view',
group: 'action',
priority: 9,
showInStatusBar: true,
appliesTo: (objectPath, view = {}) => {
if (view.getViewContext) {
let viewContext = view.getViewContext();
return viewContext.onlyAppliesToTestCase;
}
return false;
},
invoke: () => {
}
}
};
actionCollection = new ActionCollection(mockApplicableActions, mockObjectPath, mockView, openmct);
});
afterEach(() => {
actionCollection.destroy();
resetApplicationState(openmct);
});
describe("disable method invoked with action keys", () => {
it("marks those actions as isDisabled", () => {
let actionKey = 'test-action-object-path';
let actionsObject = actionCollection.getActionsObject();
let action = actionsObject[actionKey];
expect(action.isDisabled).toBeFalsy();
actionCollection.disable([actionKey]);
actionsObject = actionCollection.getActionsObject();
action = actionsObject[actionKey];
expect(action.isDisabled).toBeTrue();
});
});
describe("enable method invoked with action keys", () => {
it("marks the isDisabled property as false", () => {
let actionKey = 'test-action-object-path';
actionCollection.disable([actionKey]);
let actionsObject = actionCollection.getActionsObject();
let action = actionsObject[actionKey];
expect(action.isDisabled).toBeTrue();
actionCollection.enable([actionKey]);
actionsObject = actionCollection.getActionsObject();
action = actionsObject[actionKey];
expect(action.isDisabled).toBeFalse();
});
});
describe("hide method invoked with action keys", () => {
it("marks those actions as isHidden", () => {
let actionKey = 'test-action-object-path';
let actionsObject = actionCollection.getActionsObject();
let action = actionsObject[actionKey];
expect(action.isHidden).toBeFalsy();
actionCollection.hide([actionKey]);
actionsObject = actionCollection.getActionsObject();
action = actionsObject[actionKey];
expect(action.isHidden).toBeTrue();
});
});
describe("show method invoked with action keys", () => {
it("marks the isHidden property as false", () => {
let actionKey = 'test-action-object-path';
actionCollection.hide([actionKey]);
let actionsObject = actionCollection.getActionsObject();
let action = actionsObject[actionKey];
expect(action.isHidden).toBeTrue();
actionCollection.show([actionKey]);
actionsObject = actionCollection.getActionsObject();
action = actionsObject[actionKey];
expect(action.isHidden).toBeFalse();
});
});
describe("getVisibleActions method", () => {
it("returns an array of non hidden actions", () => {
let action1Key = 'test-action-object-path';
let action2Key = 'test-action-view';
actionCollection.hide([action1Key]);
let visibleActions = actionCollection.getVisibleActions();
expect(Array.isArray(visibleActions)).toBeTrue();
expect(visibleActions.length).toEqual(1);
expect(visibleActions[0].key).toEqual(action2Key);
actionCollection.show([action1Key]);
visibleActions = actionCollection.getVisibleActions();
expect(visibleActions.length).toEqual(2);
});
});
describe("getStatusBarActions method", () => {
it("returns an array of non disabled, non hidden statusBar actions", () => {
let action2Key = 'test-action-view';
let statusBarActions = actionCollection.getStatusBarActions();
expect(Array.isArray(statusBarActions)).toBeTrue();
expect(statusBarActions.length).toEqual(1);
expect(statusBarActions[0].key).toEqual(action2Key);
actionCollection.disable([action2Key]);
statusBarActions = actionCollection.getStatusBarActions();
expect(statusBarActions.length).toEqual(0);
actionCollection.enable([action2Key]);
statusBarActions = actionCollection.getStatusBarActions();
expect(statusBarActions.length).toEqual(1);
expect(statusBarActions[0].key).toEqual(action2Key);
actionCollection.hide([action2Key]);
statusBarActions = actionCollection.getStatusBarActions();
expect(statusBarActions.length).toEqual(0);
});
});
});

View File

@@ -22,22 +22,41 @@
import ActionsAPI from './ActionsAPI';
import { createOpenMct, resetApplicationState } from '../../utils/testing';
import ActionCollection from './ActionCollection';
describe('The Actions API', () => {
let openmct;
let actionsAPI;
let mockAction;
let mockObjectPath;
let mockObjectPathAction;
let mockViewContext1;
beforeEach(() => {
openmct = createOpenMct();
actionsAPI = new ActionsAPI(openmct);
mockObjectPathAction = {
name: 'Test Action Object Path',
key: 'test-action-object-path',
cssClass: 'test-action-object-path',
description: 'This is a test action for object path',
group: 'action',
priority: 9,
appliesTo: (objectPath) => {
if (objectPath.length) {
return objectPath[0].type === 'fake-folder';
}
return false;
},
invoke: () => {
}
};
mockAction = {
name: 'Test Action',
key: 'test-action',
cssClass: 'test-action',
description: 'This is a test action',
name: 'Test Action View',
key: 'test-action-view',
cssClass: 'test-action-view',
description: 'This is a test action for view',
group: 'action',
priority: 9,
appliesTo: (objectPath, view = {}) => {
@@ -45,8 +64,6 @@ describe('The Actions API', () => {
let viewContext = view.getViewContext();
return viewContext.onlyAppliesToTestCase;
} else if (objectPath.length) {
return objectPath[0].type === 'fake-folder';
}
return false;
@@ -100,9 +117,32 @@ describe('The Actions API', () => {
describe("get method", () => {
beforeEach(() => {
actionsAPI.register(mockAction);
actionsAPI.register(mockObjectPathAction);
});
it("returns an object with relevant actions when invoked with objectPath only", () => {
it("returns an ActionCollection when invoked with an objectPath only", () => {
let actionCollection = actionsAPI.get(mockObjectPath);
let instanceOfActionCollection = actionCollection instanceof ActionCollection;
expect(instanceOfActionCollection).toBeTrue();
});
it("returns an ActionCollection when invoked with an objectPath and view", () => {
let actionCollection = actionsAPI.get(mockObjectPath, mockViewContext1);
let instanceOfActionCollection = actionCollection instanceof ActionCollection;
expect(instanceOfActionCollection).toBeTrue();
});
it("returns relevant actions when invoked with objectPath only", () => {
let actionCollection = actionsAPI.get(mockObjectPath);
let action = actionCollection.getActionsObject()[mockObjectPathAction.key];
expect(action.key).toEqual(mockObjectPathAction.key);
expect(action.name).toEqual(mockObjectPathAction.name);
});
it("returns relevant actions when invoked with objectPath and view", () => {
let actionCollection = actionsAPI.get(mockObjectPath, mockViewContext1);
let action = actionCollection.getActionsObject()[mockAction.key];

View File

@@ -89,7 +89,7 @@ export default class DuplicateAction {
{
key: "name",
control: "textfield",
name: "Folder Name",
name: "Name",
pattern: "\\S+",
required: true,
cssClass: "l-input-lg"

View File

@@ -48,13 +48,14 @@ export default class DuplicateTask {
}
/**
* Execute the duplicate/copy task with the objects provided in the constructor.
* Execute the duplicate/copy task with the objects provided.
* @returns {promise} Which will resolve with a clone of the object
* once complete.
*/
async duplicate(domainObject, parent, filter) {
this.domainObject = domainObject;
this.parent = parent;
this.namespace = parent.identifier.namespace;
this.filter = filter || this.isCreatable;
await this.buildDuplicationPlan();
@@ -96,13 +97,14 @@ export default class DuplicateTask {
let initialCount = this.clones.length;
let dialog = this.openmct.overlays.progressDialog({
progressPerc: 0,
message: `Duplicating ${initialCount} files.`,
message: `Duplicating ${initialCount} objects.`,
iconClass: 'info',
title: 'Duplicating'
});
let clonesDone = Promise.all(this.clones.map(clone => {
let clonesDone = Promise.all(this.clones.map((clone) => {
let percentPersisted = Math.ceil(100 * (++this.persisted / initialCount));
let message = `Duplicating ${initialCount - this.persisted} files.`;
let message = `Duplicating ${initialCount - this.persisted} objects.`;
dialog.updateProgress(percentPersisted, message);
@@ -110,6 +112,7 @@ export default class DuplicateTask {
}));
await clonesDone;
dialog.dismiss();
this.openmct.notifications.info(`Duplicated ${this.persisted} objects.`);
@@ -141,10 +144,7 @@ export default class DuplicateTask {
async duplicateObject(originalObject) {
// Check if the creatable (or other passed in filter).
if (this.filter(originalObject)) {
// Clone original object
let clone = this.cloneObjectModel(originalObject);
// Get children, if any
let composeesCollection = this.openmct.composition.get(originalObject);
let composees;
@@ -152,7 +152,6 @@ export default class DuplicateTask {
composees = await composeesCollection.load();
}
// Recursively duplicate children
return this.duplicateComposees(clone, composees);
}
@@ -198,12 +197,11 @@ export default class DuplicateTask {
*/
async duplicateComposees(clonedParent, composees = []) {
let idMap = {};
let allComposeesDuplicated = composees.reduce(async (previousPromise, nextComposee) => {
await previousPromise;
let clonedComposee = await this.duplicateObject(nextComposee);
idMap[this.getId(nextComposee)] = this.getId(clonedComposee);
await this.composeChild(clonedComposee, clonedParent, clonedComposee !== nextComposee);
this.composeChild(clonedComposee, clonedParent, clonedComposee !== nextComposee);
return;
}, Promise.resolve());
@@ -216,11 +214,9 @@ export default class DuplicateTask {
return clonedParent;
}
async composeChild(child, parent, setLocation) {
const PERSIST_BOOL = false;
let parentComposition = this.openmct.composition.get(parent);
await parentComposition.load();
parentComposition.add(child, PERSIST_BOOL);
composeChild(child, parent, setLocation) {
let childKeyString = this.openmct.objects.makeKeyString(child.identifier);
parent.composition.push(childKeyString);
//If a location is not specified, set it.
if (setLocation && child.location === undefined) {
@@ -239,7 +235,7 @@ export default class DuplicateTask {
let clone = JSON.parse(JSON.stringify(domainObject));
let identifier = {
key: uuid(),
namespace: domainObject.identifier.namespace
namespace: this.namespace // set to NEW parent's namespace
};
if (clone.modified || clone.persisted || clone.location) {

View File

@@ -143,9 +143,7 @@ export default {
this.openmct.notifications.alert(message);
}
const link = `${location.host}${location.pathname}${hash}`;
const url = new URL(link);
window.location.href = url.hash;
window.location.hash = hash;
},
formatTime(unixTime, timeFormat) {
return Moment.utc(unixTime).format(timeFormat);

View File

@@ -117,8 +117,10 @@ define(
* @returns {promise}
*/
ExportImageService.prototype.exportJPG = function (element, filename, className) {
const processedFilename = replaceDotsWithUnderscores(filename);
return this.renderElement(element, "jpg", className).then(function (img) {
saveAs(img, filename);
saveAs(img, processedFilename);
});
};
@@ -130,8 +132,10 @@ define(
* @returns {promise}
*/
ExportImageService.prototype.exportPNG = function (element, filename, className) {
const processedFilename = replaceDotsWithUnderscores(filename);
return this.renderElement(element, "png", className).then(function (img) {
saveAs(img, filename);
saveAs(img, processedFilename);
});
};
@@ -146,6 +150,12 @@ define(
return this.renderElement(element, "png", className);
};
function replaceDotsWithUnderscores(filename) {
const regex = /\./gi;
return filename.replace(regex, '_');
}
/**
* canvas.toBlob() not supported in IE < 10, Opera, and Safari. This polyfill
* implements the method in browsers that would not otherwise support it.

View File

@@ -30,13 +30,13 @@ let exportCSV = {
},
group: 'view'
};
let exportMarkedRows = {
let exportMarkedDataAsCSV = {
name: 'Export Marked Rows',
key: 'export-csv-marked',
description: "Export marked rows as CSV",
cssClass: 'icon-download labeled',
invoke: (objectPath, viewProvider) => {
viewProvider.getViewContext().exportMarkedRows();
viewProvider.getViewContext().exportMarkedDataAsCSV();
},
group: 'view'
};
@@ -98,7 +98,7 @@ let autosizeColumns = {
let viewActions = [
exportCSV,
exportMarkedRows,
exportMarkedDataAsCSV,
unmarkAllRows,
pause,
play,

View File

@@ -960,7 +960,7 @@ export default {
return {
type: 'telemetry-table',
exportAllDataAsCSV: this.exportAllDataAsCSV,
exportMarkedRows: this.exportMarkedRows,
exportMarkedDataAsCSV: this.exportMarkedDataAsCSV,
unmarkAllRows: this.unmarkAllRows,
togglePauseByButton: this.togglePauseByButton,
expandColumns: this.recalculateColumnWidths,

View File

@@ -70,11 +70,8 @@ define(
if (multiSelect) {
this.handleMultiSelect(selectable);
} else {
this.setSelectionStyles(selectable);
this.selected = [selectable];
this.handleSingleSelect(selectable);
}
this.emit('change', this.selected);
};
/**
@@ -87,6 +84,20 @@ define(
this.addSelectionAttributes(selectable);
this.selected.push(selectable);
}
this.emit('change', this.selected);
};
/**
* @private
*/
Selection.prototype.handleSingleSelect = function (selectable) {
if (!_.isEqual([selectable], this.selected)) {
this.setSelectionStyles(selectable);
this.selected = [selectable];
this.emit('change', this.selected);
}
};
/**

View File

@@ -162,5 +162,5 @@
// This element is the recipient for object styling; cannot be display: contents
flex: 1 1 auto;
overflow: hidden;
display: contents;
display: block;
}

View File

@@ -15,6 +15,7 @@
v-if="handle"
class="l-pane__handle"
@mousedown="start"
@contextmenu.prevent
></div>
<div class="l-pane__header">
<span v-if="label"