Three Dot Menu Prototype (#3325)

* Three dot menu implementation

Co-authored-by: Deep Tailor <deep.j.tailor@nasa.gov>
Co-authored-by: Nikhil <nikhil.k.mandlik@nasa.gov>
This commit is contained in:
Charles Hacskaylo 2020-11-19 09:53:06 -08:00 committed by GitHub
parent d232dacc65
commit 6375ecda34
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
98 changed files with 2425 additions and 683 deletions

View File

@ -143,8 +143,8 @@ define([
"$window"
],
"group": "windowing",
"cssClass": "icon-new-window",
"priority": "preferred"
"priority": 10,
"cssClass": "icon-new-window"
}
],
"runs": [

View File

@ -139,7 +139,9 @@ define([
],
"description": "Edit",
"category": "view-control",
"cssClass": "major icon-pencil"
"cssClass": "major icon-pencil",
"group": "action",
"priority": 10
},
{
"key": "properties",
@ -150,6 +152,8 @@ define([
"implementation": PropertiesAction,
"cssClass": "major icon-pencil",
"name": "Edit Properties...",
"group": "action",
"priority": 10,
"description": "Edit properties of this object.",
"depends": [
"dialogService"

View File

@ -20,12 +20,12 @@
at runtime from the About dialog for additional information.
-->
<div class="c-object-label"
ng-class="{ 'is-missing': model.status === 'missing' }"
ng-class="{ 'is-status--missing': model.status === 'missing' }"
>
<div class="c-object-label__type-icon {{type.getCssClass()}}"
ng-class="{ 'l-icon-link':location.isLink() }"
>
<span class="is-missing__indicator" title="This item is missing"></span>
<span class="is-status__indicator" title="This item is missing or suspect"></span>
</div>
<div class='c-object-label__name'>{{model.name}}</div>
</div>

View File

@ -66,6 +66,8 @@ define([
"description": "Move object to another location.",
"cssClass": "icon-move",
"category": "contextual",
"group": "action",
"priority": 9,
"implementation": MoveAction,
"depends": [
"policyService",
@ -79,6 +81,8 @@ define([
"description": "Duplicate object to another location.",
"cssClass": "icon-duplicate",
"category": "contextual",
"group": "action",
"priority": 8,
"implementation": CopyAction,
"depends": [
"$log",
@ -95,6 +99,8 @@ define([
"description": "Create Link to object in another location.",
"cssClass": "icon-link",
"category": "contextual",
"group": "action",
"priority": 7,
"implementation": LinkAction,
"depends": [
"policyService",

View File

@ -47,6 +47,8 @@ define([
"implementation": ExportAsJSONAction,
"category": "contextual",
"cssClass": "icon-export",
"group": "json",
"priority": 2,
"depends": [
"openmct",
"exportService",
@ -61,6 +63,8 @@ define([
"implementation": ImportAsJSONAction,
"category": "contextual",
"cssClass": "icon-import",
"group": "json",
"priority": 2,
"depends": [
"exportService",
"identifierService",

View File

@ -242,7 +242,11 @@ define([
this.overlays = new OverlayAPI.default();
this.contextMenu = new api.ContextMenuRegistry();
this.menus = new api.MenuAPI(this);
this.actions = new api.ActionsAPI(this);
this.status = new api.StatusAPI(this);
this.router = new ApplicationRouter();
@ -271,6 +275,7 @@ define([
this.install(this.plugins.URLTimeSettingsSynchronizer());
this.install(this.plugins.NotificationIndicator());
this.install(this.plugins.NewFolderAction());
this.install(this.plugins.ViewDatumAction());
}
MCT.prototype = Object.create(EventEmitter.prototype);

View File

@ -35,5 +35,5 @@ export default function LegacyActionAdapter(openmct, legacyActions) {
legacyActions.filter(contextualCategoryOnly)
.map(LegacyAction => new LegacyContextMenuAction(openmct, LegacyAction))
.forEach(openmct.contextMenu.registerAction);
.forEach(openmct.actions.register);
}

View File

@ -31,6 +31,8 @@ export default class LegacyContextMenuAction {
this.description = LegacyAction.definition.description;
this.cssClass = LegacyAction.definition.cssClass;
this.LegacyAction = LegacyAction;
this.group = LegacyAction.definition.group;
this.priority = LegacyAction.definition.priority;
}
invoke(objectPath) {

View File

@ -0,0 +1,189 @@
/*****************************************************************************
* 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 EventEmitter from 'EventEmitter';
import _ from 'lodash';
class ActionCollection extends EventEmitter {
constructor(applicableActions, objectPath, view, openmct, skipEnvironmentObservers) {
super();
this.applicableActions = applicableActions;
this.openmct = openmct;
this.objectPath = objectPath;
this.view = view;
this.skipEnvironmentObservers = skipEnvironmentObservers;
this.objectUnsubscribes = [];
let debounceOptions = {
leading: false,
trailing: true
};
this._updateActions = _.debounce(this._updateActions.bind(this), 150, debounceOptions);
this._update = _.debounce(this._update.bind(this), 150, debounceOptions);
if (!skipEnvironmentObservers) {
this._observeObjectPath();
this.openmct.editor.on('isEditing', this._updateActions);
}
this._initializeActions();
}
disable(actionKeys) {
actionKeys.forEach(actionKey => {
if (this.applicableActions[actionKey]) {
this.applicableActions[actionKey].isDisabled = true;
}
});
this._update();
}
enable(actionKeys) {
actionKeys.forEach(actionKey => {
if (this.applicableActions[actionKey]) {
this.applicableActions[actionKey].isDisabled = false;
}
});
this._update();
}
hide(actionKeys) {
actionKeys.forEach(actionKey => {
if (this.applicableActions[actionKey]) {
this.applicableActions[actionKey].isHidden = true;
}
});
this._update();
}
show(actionKeys) {
actionKeys.forEach(actionKey => {
if (this.applicableActions[actionKey]) {
this.applicableActions[actionKey].isHidden = false;
}
});
this._update();
}
destroy() {
super.removeAllListeners();
if (!this.skipEnvironmentObservers) {
this.objectUnsubscribes.forEach(unsubscribe => {
unsubscribe();
});
this.openmct.editor.off('isEditing', this._updateActions);
}
this.emit('destroy', this.view);
}
getVisibleActions() {
let actionsArray = Object.keys(this.applicableActions);
let visibleActions = [];
actionsArray.forEach(actionKey => {
let action = this.applicableActions[actionKey];
if (!action.isHidden) {
visibleActions.push(action);
}
});
return visibleActions;
}
getStatusBarActions() {
let actionsArray = Object.keys(this.applicableActions);
let statusBarActions = [];
actionsArray.forEach(actionKey => {
let action = this.applicableActions[actionKey];
if (action.showInStatusBar && !action.isDisabled && !action.isHidden) {
statusBarActions.push(action);
}
});
return statusBarActions;
}
getActionsObject() {
return this.applicableActions;
}
_update() {
this.emit('update', this.applicableActions);
}
_observeObjectPath() {
let actionCollection = this;
function updateObject(oldObject, newObject) {
Object.assign(oldObject, newObject);
actionCollection._updateActions();
}
this.objectPath.forEach(object => {
if (object) {
let unsubscribe = this.openmct.objects.observe(object, '*', updateObject.bind(this, object));
this.objectUnsubscribes.push(unsubscribe);
}
});
}
_initializeActions() {
Object.keys(this.applicableActions).forEach(key => {
this.applicableActions[key].callBack = () => {
return this.applicableActions[key].invoke(this.objectPath, this.view);
};
});
}
_updateActions() {
let newApplicableActions = this.openmct.actions._applicableActions(this.objectPath, this.view);
this.applicableActions = this._mergeOldAndNewActions(this.applicableActions, newApplicableActions);
this._initializeActions();
this._update();
}
_mergeOldAndNewActions(oldActions, newActions) {
let mergedActions = {};
Object.keys(newActions).forEach(key => {
if (oldActions[key]) {
mergedActions[key] = oldActions[key];
} else {
mergedActions[key] = newActions[key];
}
});
return mergedActions;
}
}
export default ActionCollection;

View File

@ -0,0 +1,144 @@
/*****************************************************************************
* 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 EventEmitter from 'EventEmitter';
import ActionCollection from './ActionCollection';
import _ from 'lodash';
class ActionsAPI extends EventEmitter {
constructor(openmct) {
super();
this._allActions = {};
this._actionCollections = new WeakMap();
this._openmct = openmct;
this._groupOrder = ['windowing', 'undefined', 'view', 'action', 'json'];
this.register = this.register.bind(this);
this.get = this.get.bind(this);
this._applicableActions = this._applicableActions.bind(this);
this._updateCachedActionCollections = this._updateCachedActionCollections.bind(this);
}
register(actionDefinition) {
this._allActions[actionDefinition.key] = actionDefinition;
}
get(objectPath, view) {
if (view) {
return this._getCachedActionCollection(objectPath, view) || this._newActionCollection(objectPath, view, true);
} else {
return this._newActionCollection(objectPath, view, true);
}
}
updateGroupOrder(groupArray) {
this._groupOrder = groupArray;
}
_get(objectPath, view) {
let actionCollection = this._newActionCollection(objectPath, view);
this._actionCollections.set(view, actionCollection);
actionCollection.on('destroy', this._updateCachedActionCollections);
return actionCollection;
}
_getCachedActionCollection(objectPath, view) {
let cachedActionCollection = this._actionCollections.get(view);
return cachedActionCollection;
}
_newActionCollection(objectPath, view, skipEnvironmentObservers) {
let applicableActions = this._applicableActions(objectPath, view);
return new ActionCollection(applicableActions, objectPath, view, this._openmct, skipEnvironmentObservers);
}
_updateCachedActionCollections(key) {
if (this._actionCollections.has(key)) {
let actionCollection = this._actionCollections.get(key);
actionCollection.off('destroy', this._updateCachedActionCollections);
this._actionCollections.delete(key);
}
}
_applicableActions(objectPath, view) {
let actionsObject = {};
let keys = Object.keys(this._allActions).filter(key => {
let actionDefinition = this._allActions[key];
if (actionDefinition.appliesTo === undefined) {
return true;
} else {
return actionDefinition.appliesTo(objectPath, view);
}
});
keys.forEach(key => {
let action = _.clone(this._allActions[key]);
actionsObject[key] = action;
});
return actionsObject;
}
_groupAndSortActions(actionsArray) {
if (!Array.isArray(actionsArray) && typeof actionsArray === 'object') {
actionsArray = Object.keys(actionsArray).map(key => actionsArray[key]);
}
let actionsObject = {};
let groupedSortedActionsArray = [];
function sortDescending(a, b) {
return b.priority - a.priority;
}
actionsArray.forEach(action => {
if (actionsObject[action.group] === undefined) {
actionsObject[action.group] = [action];
} else {
actionsObject[action.group].push(action);
}
});
this._groupOrder.forEach(group => {
let groupArray = actionsObject[group];
if (groupArray) {
groupedSortedActionsArray.push(groupArray.sort(sortDescending));
}
});
return groupedSortedActionsArray;
}
}
export default ActionsAPI;

View File

@ -0,0 +1,113 @@
/*****************************************************************************
* 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 ActionsAPI from './ActionsAPI';
import { createOpenMct, resetApplicationState } from '../../utils/testing';
describe('The Actions API', () => {
let openmct;
let actionsAPI;
let mockAction;
let mockObjectPath;
let mockViewContext1;
beforeEach(() => {
openmct = createOpenMct();
actionsAPI = new ActionsAPI(openmct);
mockAction = {
name: 'Test Action',
key: 'test-action',
cssClass: 'test-action',
description: 'This is a test action',
group: 'action',
priority: 9,
appliesTo: (objectPath, view = {}) => {
if (view.getViewContext) {
let viewContext = view.getViewContext();
return viewContext.onlyAppliesToTestCase;
} else if (objectPath.length) {
return objectPath[0].type === 'fake-folder';
}
return false;
},
invoke: () => {
}
};
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: ''
}
}
];
mockViewContext1 = {
getViewContext: () => {
return {
onlyAppliesToTestCase: true
};
}
};
});
afterEach(() => {
resetApplicationState(openmct);
});
describe("register method", () => {
it("adds action to ActionsAPI", () => {
actionsAPI.register(mockAction);
let actionCollection = actionsAPI.get(mockObjectPath, mockViewContext1);
let action = actionCollection.getActionsObject()[mockAction.key];
expect(action.key).toEqual(mockAction.key);
expect(action.name).toEqual(mockAction.name);
});
});
describe("get method", () => {
beforeEach(() => {
actionsAPI.register(mockAction);
});
it("returns an object with relevant actions when invoked with objectPath only", () => {
let actionCollection = actionsAPI.get(mockObjectPath, mockViewContext1);
let action = actionCollection.getActionsObject()[mockAction.key];
expect(action.key).toEqual(mockAction.key);
expect(action.name).toEqual(mockAction.name);
});
});
});

View File

@ -28,9 +28,10 @@ define([
'./telemetry/TelemetryAPI',
'./indicators/IndicatorAPI',
'./notifications/NotificationAPI',
'./contextMenu/ContextMenuAPI',
'./Editor'
'./Editor',
'./menu/MenuAPI',
'./actions/ActionsAPI',
'./status/StatusAPI'
], function (
TimeAPI,
ObjectAPI,
@ -39,8 +40,10 @@ define([
TelemetryAPI,
IndicatorAPI,
NotificationAPI,
ContextMenuAPI,
EditorAPI
EditorAPI,
MenuAPI,
ActionsAPI,
StatusAPI
) {
return {
TimeAPI: TimeAPI,
@ -51,6 +54,8 @@ define([
IndicatorAPI: IndicatorAPI,
NotificationAPI: NotificationAPI.default,
EditorAPI: EditorAPI,
ContextMenuRegistry: ContextMenuAPI.default
MenuAPI: MenuAPI.default,
ActionsAPI: ActionsAPI.default,
StatusAPI: StatusAPI.default
};
});

View File

@ -1,24 +0,0 @@
<template>
<div class="c-menu">
<ul>
<li
v-for="action in actions"
:key="action.name"
:class="action.cssClass"
:title="action.description"
@click="action.invoke(objectPath)"
>
{{ action.name }}
</li>
<li v-if="actions.length === 0">
No actions defined.
</li>
</ul>
</div>
</template>
<script>
export default {
inject: ['actions', 'objectPath']
};
</script>

View File

@ -1,159 +0,0 @@
/*****************************************************************************
* 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 ContextMenuComponent from './ContextMenu.vue';
import Vue from 'vue';
/**
* The ContextMenuAPI allows the addition of new context menu actions, and for the context menu to be launched from
* custom HTML elements.
* @interface ContextMenuAPI
* @memberof module:openmct
*/
class ContextMenuAPI {
constructor() {
this._allActions = [];
this._activeContextMenu = undefined;
this._hideActiveContextMenu = this._hideActiveContextMenu.bind(this);
this.registerAction = this.registerAction.bind(this);
}
/**
* Defines an item to be added to context menus. Allows specification of text, appearance, and behavior when
* selected. Applicabilioty can be restricted by specification of an `appliesTo` function.
*
* @interface ContextMenuAction
* @memberof module:openmct
* @property {string} name the human-readable name of this view
* @property {string} description a longer-form description (typically
* a single sentence or short paragraph) of this kind of view
* @property {string} cssClass the CSS class to apply to labels for this
* view (to add icons, for instance)
* @property {string} key unique key to identify the context menu action
* (used in custom context menu eg table rows, to identify which actions to include)
* @property {boolean} hideInDefaultMenu optional flag to hide action from showing in the default context menu (tree item)
*/
/**
* @method appliesTo
* @memberof module:openmct.ContextMenuAction#
* @param {DomainObject[]} objectPath the path of the object that the context menu has been invoked on.
* @returns {boolean} true if the action applies to the objects specified in the 'objectPath', otherwise false.
*/
/**
* Code to be executed when the action is selected from a context menu
* @method invoke
* @memberof module:openmct.ContextMenuAction#
* @param {DomainObject[]} objectPath the path of the object to invoke the action on.
*/
/**
* @param {ContextMenuAction} actionDefinition
*/
registerAction(actionDefinition) {
this._allActions.push(actionDefinition);
}
/**
* @private
*/
_showContextMenuForObjectPath(objectPath, x, y, actionsToBeIncluded) {
let applicableActions = this._allActions.filter((action) => {
if (actionsToBeIncluded) {
if (action.appliesTo === undefined && actionsToBeIncluded.includes(action.key)) {
return true;
}
return action.appliesTo(objectPath, actionsToBeIncluded) && actionsToBeIncluded.includes(action.key);
} else {
if (action.appliesTo === undefined) {
return true;
}
return action.appliesTo(objectPath) && !action.hideInDefaultMenu;
}
});
if (this._activeContextMenu) {
this._hideActiveContextMenu();
}
this._activeContextMenu = this._createContextMenuForObject(objectPath, applicableActions);
this._activeContextMenu.$mount();
document.body.appendChild(this._activeContextMenu.$el);
let position = this._calculatePopupPosition(x, y, this._activeContextMenu.$el);
this._activeContextMenu.$el.style.left = `${position.x}px`;
this._activeContextMenu.$el.style.top = `${position.y}px`;
document.addEventListener('click', this._hideActiveContextMenu);
}
/**
* @private
*/
_calculatePopupPosition(eventPosX, eventPosY, menuElement) {
let menuDimensions = menuElement.getBoundingClientRect();
let overflowX = (eventPosX + menuDimensions.width) - document.body.clientWidth;
let overflowY = (eventPosY + menuDimensions.height) - document.body.clientHeight;
if (overflowX > 0) {
eventPosX = eventPosX - overflowX;
}
if (overflowY > 0) {
eventPosY = eventPosY - overflowY;
}
return {
x: eventPosX,
y: eventPosY
};
}
/**
* @private
*/
_hideActiveContextMenu() {
document.removeEventListener('click', this._hideActiveContextMenu);
document.body.removeChild(this._activeContextMenu.$el);
this._activeContextMenu.$destroy();
this._activeContextMenu = undefined;
}
/**
* @private
*/
_createContextMenuForObject(objectPath, actions) {
return new Vue({
components: {
ContextMenu: ContextMenuComponent
},
provide: {
actions: actions,
objectPath: objectPath
},
template: '<ContextMenu></ContextMenu>'
});
}
}
export default ContextMenuAPI;

67
src/api/menu/MenuAPI.js Normal file
View File

@ -0,0 +1,67 @@
/*****************************************************************************
* 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 Menu from './menu.js';
/**
* The MenuAPI allows the addition of new context menu actions, and for the context menu to be launched from
* custom HTML elements.
* @interface MenuAPI
* @memberof module:openmct
*/
class MenuAPI {
constructor(openmct) {
this.openmct = openmct;
this.showMenu = this.showMenu.bind(this);
this._clearMenuComponent = this._clearMenuComponent.bind(this);
this._showObjectMenu = this._showObjectMenu.bind(this);
}
showMenu(x, y, actions) {
if (this.menuComponent) {
this.menuComponent.dismiss();
}
let options = {
x,
y,
actions
};
this.menuComponent = new Menu(options);
this.menuComponent.once('destroy', this._clearMenuComponent);
}
_clearMenuComponent() {
this.menuComponent = undefined;
delete this.menuComponent;
}
_showObjectMenu(objectPath, x, y, actionsToBeIncluded) {
let applicableActions = this.openmct.actions._groupedAndSortedObjectActions(objectPath, actionsToBeIncluded);
this.showMenu(x, y, applicableActions);
}
}
export default MenuAPI;

125
src/api/menu/MenuAPISpec.js Normal file
View File

@ -0,0 +1,125 @@
/*****************************************************************************
* 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 MenuAPI from './MenuAPI';
import Menu from './menu';
import { createOpenMct, resetApplicationState } from '../../utils/testing';
describe ('The Menu API', () => {
let openmct;
let menuAPI;
let actionsArray;
let x;
let y;
let result;
beforeEach(() => {
openmct = createOpenMct();
menuAPI = new MenuAPI(openmct);
actionsArray = [
{
name: 'Test Action 1',
cssClass: 'test-css-class-1',
description: 'This is a test action',
callBack: () => {
result = 'Test Action 1 Invoked';
}
},
{
name: 'Test Action 2',
cssClass: 'test-css-class-2',
description: 'This is a test action',
callBack: () => {
result = 'Test Action 2 Invoked';
}
}
];
x = 8;
y = 16;
});
afterEach(() => {
resetApplicationState(openmct);
});
describe("showMenu method", () => {
it("creates an instance of Menu when invoked", () => {
menuAPI.showMenu(x, y, actionsArray);
expect(menuAPI.menuComponent).toBeInstanceOf(Menu);
});
describe("creates a menu component", () => {
let menuComponent;
let vueComponent;
beforeEach(() => {
menuAPI.showMenu(x, y, actionsArray);
vueComponent = menuAPI.menuComponent.component;
menuComponent = document.querySelector(".c-menu");
spyOn(vueComponent, '$destroy');
});
it("renders a menu component in the expected x and y coordinates", () => {
let boundingClientRect = menuComponent.getBoundingClientRect();
let left = boundingClientRect.left;
let top = boundingClientRect.top;
expect(left).toEqual(x);
expect(top).toEqual(y);
});
it("with all the actions passed in", () => {
expect(menuComponent).toBeDefined();
let listItems = menuComponent.children[0].children;
expect(listItems.length).toEqual(actionsArray.length);
});
it("with click-able menu items, that will invoke the correct callBacks", () => {
let listItem1 = menuComponent.children[0].children[0];
listItem1.click();
expect(result).toEqual("Test Action 1 Invoked");
});
it("dismisses the menu when action is clicked on", () => {
let listItem1 = menuComponent.children[0].children[0];
listItem1.click();
let menu = document.querySelector('.c-menu');
expect(menu).toBeNull();
});
it("invokes the destroy method when menu is dismissed", () => {
document.body.click();
expect(vueComponent.$destroy).toHaveBeenCalled();
});
});
});
});

View File

@ -0,0 +1,52 @@
<template>
<div class="c-menu">
<ul v-if="actions.length && actions[0].length">
<template
v-for="(actionGroups, index) in actions"
>
<li
v-for="action in actionGroups"
:key="action.name"
:class="[action.cssClass, action.isDisabled ? 'disabled' : '']"
:title="action.description"
@click="action.callBack"
>
{{ action.name }}
</li>
<div
v-if="index !== actions.length - 1"
:key="index"
class="c-menu__section-separator"
>
</div>
<li
v-if="actionGroups.length === 0"
:key="index"
>
No actions defined.
</li>
</template>
</ul>
<ul v-else>
<li
v-for="action in actions"
:key="action.name"
:class="action.cssClass"
:title="action.description"
@click="action.callBack"
>
{{ action.name }}
</li>
<li v-if="actions.length === 0">
No actions defined.
</li>
</ul>
</div>
</template>
<script>
export default {
inject: ['actions']
};
</script>

94
src/api/menu/menu.js Normal file
View File

@ -0,0 +1,94 @@
/*****************************************************************************
* 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 EventEmitter from 'EventEmitter';
import MenuComponent from './components/Menu.vue';
import Vue from 'vue';
class Menu extends EventEmitter {
constructor(options) {
super();
this.options = options;
this.component = new Vue({
provide: {
actions: options.actions
},
components: {
MenuComponent
},
template: '<menu-component />'
});
if (options.onDestroy) {
this.once('destroy', options.onDestroy);
}
this.dismiss = this.dismiss.bind(this);
this.show = this.show.bind(this);
this.show();
}
dismiss() {
this.emit('destroy');
document.body.removeChild(this.component.$el);
document.removeEventListener('click', this.dismiss);
this.component.$destroy();
}
show() {
this.component.$mount();
document.body.appendChild(this.component.$el);
let position = this._calculatePopupPosition(this.options.x, this.options.y, this.component.$el);
this.component.$el.style.left = `${position.x}px`;
this.component.$el.style.top = `${position.y}px`;
document.addEventListener('click', this.dismiss);
}
/**
* @private
*/
_calculatePopupPosition(eventPosX, eventPosY, menuElement) {
let menuDimensions = menuElement.getBoundingClientRect();
let overflowX = (eventPosX + menuDimensions.width) - document.body.clientWidth;
let overflowY = (eventPosY + menuDimensions.height) - document.body.clientHeight;
if (overflowX > 0) {
eventPosX = eventPosX - overflowX;
}
if (overflowY > 0) {
eventPosY = eventPosY - overflowY;
}
return {
x: eventPosX,
y: eventPosY
};
}
}
export default Menu;

View File

@ -22,6 +22,7 @@ class OverlayAPI {
this.dismissLastOverlay();
}
});
}
/**
@ -127,6 +128,7 @@ class OverlayAPI {
return progressDialog;
}
}
export default OverlayAPI;

View File

@ -0,0 +1,67 @@
/*****************************************************************************
* 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 EventEmitter from 'EventEmitter';
export default class StatusAPI extends EventEmitter {
constructor(openmct) {
super();
this._openmct = openmct;
this._statusCache = {};
this.get = this.get.bind(this);
this.set = this.set.bind(this);
this.observe = this.observe.bind(this);
}
get(identifier) {
let keyString = this._openmct.objects.makeKeyString(identifier);
return this._statusCache[keyString];
}
set(identifier, value) {
let keyString = this._openmct.objects.makeKeyString(identifier);
this._statusCache[keyString] = value;
this.emit(keyString, value);
}
delete(identifier) {
let keyString = this._openmct.objects.makeKeyString(identifier);
this._statusCache[keyString] = undefined;
this.emit(keyString, undefined);
delete this._statusCache[keyString];
}
observe(identifier, callback) {
let key = this._openmct.objects.makeKeyString(identifier);
this.on(key, callback);
return () => {
this.off(key, callback);
};
}
}

View File

@ -0,0 +1,85 @@
import StatusAPI from './StatusAPI.js';
import { createOpenMct, resetApplicationState } from '../../utils/testing';
describe("The Status API", () => {
let statusAPI;
let openmct;
let identifier;
let status;
let status2;
let callback;
beforeEach(() => {
openmct = createOpenMct();
statusAPI = new StatusAPI(openmct);
identifier = {
namespace: "test-namespace",
key: "test-key"
};
status = "test-status";
status2 = 'test-status-deux';
callback = jasmine.createSpy('callback', (statusUpdate) => statusUpdate);
});
afterEach(() => {
resetApplicationState(openmct);
});
describe("set function", () => {
it("sets status for identifier", () => {
statusAPI.set(identifier, status);
let resultingStatus = statusAPI.get(identifier);
expect(resultingStatus).toEqual(status);
});
});
describe("get function", () => {
it("returns status for identifier", () => {
statusAPI.set(identifier, status2);
let resultingStatus = statusAPI.get(identifier);
expect(resultingStatus).toEqual(status2);
});
});
describe("delete function", () => {
it("deletes status for identifier", () => {
statusAPI.set(identifier, status);
let resultingStatus = statusAPI.get(identifier);
expect(resultingStatus).toEqual(status);
statusAPI.delete(identifier);
resultingStatus = statusAPI.get(identifier);
expect(resultingStatus).toBeUndefined();
});
});
describe("observe function", () => {
it("allows callbacks to be attached to status set and delete events", () => {
let unsubscribe = statusAPI.observe(identifier, callback);
statusAPI.set(identifier, status);
expect(callback).toHaveBeenCalledWith(status);
statusAPI.delete(identifier);
expect(callback).toHaveBeenCalledWith(undefined);
unsubscribe();
});
it("returns a unsubscribe function", () => {
let unsubscribe = statusAPI.observe(identifier, callback);
unsubscribe();
statusAPI.set(identifier, status);
expect(callback).toHaveBeenCalledTimes(0);
});
});
});

View File

@ -44,6 +44,7 @@
<script>
const CONTEXT_MENU_ACTIONS = [
'viewDatumAction',
'viewHistoricalData',
'remove'
];
@ -129,6 +130,7 @@ export default {
let limit;
if (this.shouldUpdate(newTimestamp)) {
this.datum = datum;
this.timestamp = newTimestamp;
this.value = this.formats[this.valueKey].format(datum);
limit = this.limitEvaluator.evaluate(datum, this.valueMetadata);
@ -175,8 +177,25 @@ export default {
this.resetValues();
this.timestampKey = timeSystem.key;
},
getView() {
return {
getViewContext: () => {
return {
viewHistoricalData: true,
viewDatumAction: true,
getDatum: () => {
return this.datum;
}
};
}
};
},
showContextMenu(event) {
this.openmct.contextMenu._showContextMenuForObjectPath(this.currentObjectPath, event.x, event.y, CONTEXT_MENU_ACTIONS);
let actionCollection = this.openmct.actions.get(this.currentObjectPath, this.getView());
let allActions = actionCollection.getActionsObject();
let applicableActions = CONTEXT_MENU_ACTIONS.map(key => allActions[key]);
this.openmct.menus.showMenu(event.x, event.y, applicableActions);
},
resetValues() {
this.value = '---';

View File

@ -23,6 +23,7 @@
export default class ClearDataAction {
constructor(openmct, appliesToObjects) {
this.name = 'Clear Data for Object';
this.key = 'clear-data-action';
this.description = 'Clears current data for object, unsubscribes and resubscribes to data';
this.cssClass = 'icon-clear-data';

View File

@ -53,7 +53,7 @@ define([
openmct.indicators.add(indicator);
}
openmct.contextMenu.registerAction(new ClearDataAction.default(openmct, appliesToObjects));
openmct.actions.register(new ClearDataAction.default(openmct, appliesToObjects));
};
};
});

View File

@ -26,12 +26,12 @@ import ClearDataAction from '../clearDataAction.js';
describe('When the Clear Data Plugin is installed,', function () {
const mockObjectViews = jasmine.createSpyObj('objectViews', ['emit']);
const mockIndicatorProvider = jasmine.createSpyObj('indicators', ['add']);
const mockContextMenuProvider = jasmine.createSpyObj('contextMenu', ['registerAction']);
const mockActionsProvider = jasmine.createSpyObj('actions', ['register']);
const openmct = {
objectViews: mockObjectViews,
indicators: mockIndicatorProvider,
contextMenu: mockContextMenuProvider,
actions: mockActionsProvider,
install: function (plugin) {
plugin(this);
}
@ -51,7 +51,7 @@ describe('When the Clear Data Plugin is installed,', function () {
it('Clear Data context menu action is installed', function () {
openmct.install(ClearDataActionPlugin([]));
expect(mockContextMenuProvider.registerAction).toHaveBeenCalled();
expect(mockActionsProvider.register).toHaveBeenCalled();
});
it('clear data action emits a clearData event when invoked', function () {

View File

@ -64,9 +64,16 @@ define([
components: {
AlphanumericFormatView: AlphanumericFormatView.default
},
template: '<alphanumeric-format-view></alphanumeric-format-view>'
template: '<alphanumeric-format-view ref="alphanumericFormatView"></alphanumeric-format-view>'
});
},
getViewContext() {
if (component) {
return component.$refs.alphanumericFormatView.getViewContext();
} else {
return {};
}
},
destroy: function () {
component.$destroy();
component = undefined;

View File

@ -0,0 +1,34 @@
import clipboard from '@/utils/clipboard';
export default class CopyToClipboardAction {
constructor(openmct) {
this.openmct = openmct;
this.cssClass = 'icon-duplicate';
this.description = 'Copy value to clipboard';
this.group = "action";
this.key = 'copyToClipboard';
this.name = 'Copy to Clipboard';
this.priority = 1;
}
invoke(objectPath, view = {}) {
const viewContext = view.getViewContext && view.getViewContext();
const formattedValue = viewContext.formattedValueForCopy();
clipboard.updateClipboard(formattedValue)
.then(() => {
this.openmct.notifications.info(`Success : copied '${formattedValue}' to clipboard `);
})
.catch(() => {
this.openmct.notifications.error(`Failed : to copy '${formattedValue}' to clipboard `);
});
}
appliesTo(objectPath, view = {}) {
let viewContext = view.getViewContext && view.getViewContext();
return viewContext && viewContext.formattedValueForCopy
&& typeof viewContext.formattedValueForCopy === 'function';
}
}

View File

@ -142,14 +142,18 @@ export default {
this.domainObject = domainObject;
this.currentObjectPath = [this.domainObject].concat(this.objectPath.slice());
this.$nextTick(() => {
let childContext = this.$refs.objectFrame.getSelectionContext();
childContext.item = domainObject;
childContext.layoutItem = this.item;
childContext.index = this.index;
this.context = childContext;
this.removeSelectable = this.openmct.selection.selectable(
this.$el, this.context, this.immediatelySelect || this.initSelect);
delete this.immediatelySelect;
let reference = this.$refs.objectFrame;
if (reference) {
let childContext = this.$refs.objectFrame.getSelectionContext();
childContext.item = domainObject;
childContext.layoutItem = this.item;
childContext.index = this.index;
this.context = childContext;
this.removeSelectable = this.openmct.selection.selectable(
this.$el, this.context, this.immediatelySelect || this.initSelect);
delete this.immediatelySelect;
}
});
}
}

View File

@ -30,18 +30,15 @@
>
<div
v-if="domainObject"
class="u-style-receiver c-telemetry-view"
:class="{
styleClass,
'is-missing': domainObject.status === 'missing'
}"
class="c-telemetry-view"
:class="[statusClass]"
:style="styleObject"
:data-font-size="item.fontSize"
:data-font="item.font"
@contextmenu.prevent="showContextMenu"
>
<div class="is-missing__indicator"
title="This item is missing"
<div class="is-status__indicator"
:title="`This item is ${status}`"
></div>
<div
v-if="showLabel"
@ -76,10 +73,11 @@
import LayoutFrame from './LayoutFrame.vue';
import printj from 'printj';
import conditionalStylesMixin from "../mixins/objectStyles-mixin";
import { getDefaultNotebook } from '@/plugins/notebook/utils/notebook-storage.js';
const DEFAULT_TELEMETRY_DIMENSIONS = [10, 5];
const DEFAULT_POSITION = [1, 1];
const CONTEXT_MENU_ACTIONS = ['viewHistoricalData'];
const CONTEXT_MENU_ACTIONS = ['copyToClipboard', 'copyToNotebook', 'viewHistoricalData'];
export default {
makeDefinition(openmct, gridSize, domainObject, position) {
@ -129,13 +127,18 @@ export default {
},
data() {
return {
currentObjectPath: undefined,
datum: undefined,
formats: undefined,
domainObject: undefined,
currentObjectPath: undefined
formats: undefined,
viewKey: `alphanumeric-format-${Math.random()}`,
status: ''
};
},
computed: {
statusClass() {
return (this.status) ? `is-status--${this.status}` : '';
},
showLabel() {
let displayMode = this.item.displayMode;
@ -213,9 +216,13 @@ export default {
this.openmct.objects.get(this.item.identifier)
.then(this.setObject);
this.openmct.time.on("bounds", this.refreshData);
this.status = this.openmct.status.get(this.item.identifier);
this.removeStatusListener = this.openmct.status.observe(this.item.identifier, this.setStatus);
},
destroyed() {
this.removeSubscription();
this.removeStatusListener();
if (this.removeSelectable) {
this.removeSelectable();
@ -224,6 +231,12 @@ export default {
this.openmct.time.off("bounds", this.refreshData);
},
methods: {
formattedValueForCopy() {
const timeFormatterKey = this.openmct.time.timeSystem().key;
const timeFormatter = this.formats[timeFormatterKey];
return `At ${timeFormatter.format(this.datum)} ${this.domainObject.name} had a value of ${this.telemetryValue} ${this.unit}`;
},
requestHistoricalData() {
let bounds = this.openmct.time.bounds();
let options = {
@ -261,6 +274,16 @@ export default {
this.requestHistoricalData(this.domainObject);
}
},
getView() {
return {
getViewContext: () => {
return {
viewHistoricalData: true,
formattedValueForCopy: this.formattedValueForCopy
};
}
};
},
setObject(domainObject) {
this.domainObject = domainObject;
this.keyString = this.openmct.objects.makeKeyString(domainObject.identifier);
@ -288,8 +311,33 @@ export default {
updateTelemetryFormat(format) {
this.$emit('formatChanged', this.item, format);
},
showContextMenu(event) {
this.openmct.contextMenu._showContextMenuForObjectPath(this.currentObjectPath, event.x, event.y, CONTEXT_MENU_ACTIONS);
async getContextMenuActions() {
const defaultNotebook = getDefaultNotebook();
const domainObject = defaultNotebook && await this.openmct.objects.get(defaultNotebook.notebookMeta.identifier);
const actionCollection = this.openmct.actions.get(this.currentObjectPath, this.getView());
const actionsObject = actionCollection.getActionsObject();
let copyToNotebookAction = actionsObject.copyToNotebook;
if (defaultNotebook) {
const defaultPath = domainObject && `${domainObject.name} - ${defaultNotebook.section.name} - ${defaultNotebook.page.name}`;
copyToNotebookAction.name = `Copy to Notebook ${defaultPath}`;
} else {
actionsObject.copyToNotebook = undefined;
delete actionsObject.copyToNotebook;
}
return CONTEXT_MENU_ACTIONS.map(actionKey => {
return actionsObject[actionKey];
}).filter(action => action !== undefined);
},
async showContextMenu(event) {
const contextMenuActions = await this.getContextMenuActions();
this.openmct.menus.showMenu(event.x, event.y, contextMenuActions);
},
setStatus(status) {
this.status = status;
}
}
};

View File

@ -7,7 +7,6 @@
flex: 1 1 auto;
display: flex;
flex-direction: row;
// justify-content: center;
align-items: center;
overflow: hidden;
padding: $interiorMargin;
@ -27,14 +26,13 @@
border: 1px solid transparent;
}
@include isMissing($absPos: true);
.is-missing__indicator {
.is-status__indicator {
position: absolute;
top: 0;
left: 0;
}
&.is-missing {
&[class*='is-status'] {
border: $borderMissing;
}
}

View File

@ -26,9 +26,12 @@ import objectUtils from 'objectUtils';
import DisplayLayoutType from './DisplayLayoutType.js';
import DisplayLayoutToolbar from './DisplayLayoutToolbar.js';
import AlphaNumericFormatViewProvider from './AlphanumericFormatViewProvider.js';
import CopyToClipboardAction from './actions/CopyToClipboardAction';
export default function DisplayLayoutPlugin(options) {
return function (openmct) {
openmct.actions.register(new CopyToClipboardAction(openmct));
openmct.objectViews.addProvider({
key: 'layout.view',
canView: function (domainObject) {

View File

@ -1,11 +1,10 @@
<template>
<a
class="l-grid-view__item c-grid-item"
:class="{
:class="[{
'is-alias': item.isAlias === true,
'is-missing': item.model.status === 'missing',
'c-grid-item--unknown': item.type.cssClass === undefined || item.type.cssClass.indexOf('unknown') !== -1
}"
}, statusClass]"
:href="objectLink"
>
<div
@ -27,8 +26,8 @@
</div>
</div>
<div class="c-grid-item__controls">
<div class="is-missing__indicator"
title="This item is missing"
<div class="is-status__indicator"
:title="`This item is ${status}`"
></div>
<div
class="icon-people"
@ -46,9 +45,10 @@
<script>
import contextMenuGesture from '../../../ui/mixins/context-menu-gesture';
import objectLink from '../../../ui/mixins/object-link';
import statusListener from './status-listener';
export default {
mixins: [contextMenuGesture, objectLink],
mixins: [contextMenuGesture, objectLink, statusListener],
props: {
item: {
type: Object,

View File

@ -8,18 +8,18 @@
<a
ref="objectLink"
class="c-object-label"
:class="{ 'is-missing': item.model.status === 'missing' }"
:class="[statusClass]"
:href="objectLink"
>
<div
class="c-object-label__type-icon c-list-item__type-icon"
class="c-object-label__type-icon c-list-item__name__type-icon"
:class="item.type.cssClass"
>
<span class="is-missing__indicator"
title="This item is missing"
<span class="is-status__indicator"
:title="`This item is ${status}`"
></span>
</div>
<div class="c-object-label__name c-list-item__name">{{ item.model.name }}</div>
<div class="c-object-label__name c-list-item__name__name">{{ item.model.name }}</div>
</a>
</td>
<td class="c-list-item__type">
@ -39,9 +39,10 @@
import moment from 'moment';
import contextMenuGesture from '../../../ui/mixins/context-menu-gesture';
import objectLink from '../../../ui/mixins/object-link';
import statusListener from './status-listener';
export default {
mixins: [contextMenuGesture, objectLink],
mixins: [contextMenuGesture, objectLink, statusListener],
props: {
item: {
type: Object,

View File

@ -43,9 +43,7 @@
}
}
&.is-missing {
@include isMissing();
&[class*='is-status'] {
[class*='__type-icon'],
[class*='__details'] {
opacity: $opacityMissing;

View File

@ -1,11 +1,19 @@
/******************************* LIST ITEM */
.c-list-item {
&__type-icon {
&__name__type-icon {
color: $colorItemTreeIcon;
}
&__name {
&__name__name {
@include ellipsize();
a & {
color: $colorItemFg;
}
}
&:not(.c-list-item__name) {
color: $colorItemFgDetails;
}
&.is-alias {

View File

@ -28,9 +28,5 @@
padding-top: $p;
padding-bottom: $p;
width: 25%;
&:not(.c-list-item__name) {
color: $colorItemFgDetails;
}
}
}

View File

@ -0,0 +1,33 @@
export default {
inject: ['openmct'],
props: {
item: {
type: Object,
required: true
}
},
computed: {
statusClass() {
return (this.status) ? `is-status--${this.status}` : '';
}
},
data() {
return {
status: ''
};
},
methods: {
setStatus(status) {
this.status = status;
}
},
mounted() {
let identifier = this.item.model.identifier;
this.status = this.openmct.status.get(identifier);
this.removeStatusListener = this.openmct.status.observe(identifier, this.setStatus);
},
destroyed() {
this.removeStatusListener();
}
};

View File

@ -25,6 +25,8 @@ export default class GoToOriginalAction {
this.name = 'Go To Original';
this.key = 'goToOriginal';
this.description = 'Go to the original unlinked instance of this object';
this.group = 'action';
this.priority = 4;
this._openmct = openmct;
}

View File

@ -23,6 +23,6 @@ import GoToOriginalAction from './goToOriginalAction';
export default function () {
return function (openmct) {
openmct.contextMenu.registerAction(new GoToOriginalAction(openmct));
openmct.actions.register(new GoToOriginalAction(openmct));
};
}

View File

@ -28,6 +28,8 @@ export default class NewFolderAction {
this.key = 'newFolder';
this.description = 'Create a new folder';
this.cssClass = 'icon-folder-new';
this.group = "action";
this.priority = 9;
this._openmct = openmct;
this._dialogForm = {

View File

@ -23,6 +23,6 @@ import NewFolderAction from './newFolderAction';
export default function () {
return function (openmct) {
openmct.contextMenu.registerAction(new NewFolderAction(openmct));
openmct.actions.register(new NewFolderAction(openmct));
};
}

View File

@ -40,9 +40,7 @@ describe("the plugin", () => {
openmct.on('start', done);
openmct.startHeadless();
newFolderAction = openmct.contextMenu._allActions.filter(action => {
return action.key === 'newFolder';
})[0];
newFolderAction = openmct.actions._allActions.newFolder;
});
afterEach(() => {

View File

@ -0,0 +1,40 @@
import { getDefaultNotebook } from '../utils/notebook-storage';
import { addNotebookEntry } from '../utils/notebook-entries';
export default class CopyToNotebookAction {
constructor(openmct) {
this.openmct = openmct;
this.cssClass = 'icon-duplicate';
this.description = 'Copy value to notebook as an entry';
this.group = "action";
this.key = 'copyToNotebook';
this.name = 'Copy to Notebook';
this.priority = 1;
}
copyToNotebook(entryText) {
const notebookStorage = getDefaultNotebook();
this.openmct.objects.get(notebookStorage.notebookMeta.identifier)
.then(domainObject => {
addNotebookEntry(this.openmct, domainObject, notebookStorage, null, entryText);
const defaultPath = `${domainObject.name} - ${notebookStorage.section.name} - ${notebookStorage.page.name}`;
const msg = `Saved to Notebook ${defaultPath}`;
this.openmct.notifications.info(msg);
});
}
invoke(objectPath, view = {}) {
let viewContext = view.getViewContext && view.getViewContext();
this.copyToNotebook(viewContext.formattedValueForCopy());
}
appliesTo(objectPath, view = {}) {
let viewContext = view.getViewContext && view.getViewContext();
return viewContext && viewContext.formattedValueForCopy
&& typeof viewContext.formattedValueForCopy === 'function';
}
}

View File

@ -111,7 +111,7 @@ import Search from '@/ui/components/search.vue';
import SearchResults from './SearchResults.vue';
import Sidebar from './Sidebar.vue';
import { clearDefaultNotebook, getDefaultNotebook, setDefaultNotebook, setDefaultNotebookSection, setDefaultNotebookPage } from '../utils/notebook-storage';
import { DEFAULT_CLASS, addNotebookEntry, createNewEmbed, getNotebookEntries, mutateObject } from '../utils/notebook-entries';
import { addNotebookEntry, createNewEmbed, getNotebookEntries, mutateObject } from '../utils/notebook-entries';
import objectUtils from 'objectUtils';
import { throttle } from 'lodash';
@ -431,14 +431,7 @@ export default {
return;
}
const classList = domainObject.classList || [];
const index = classList.indexOf(DEFAULT_CLASS);
if (!classList.length || index < 0) {
return;
}
classList.splice(index, 1);
mutateObject(this.openmct, domainObject, 'classList', classList);
this.openmct.status.delete(domainObject.identifier);
},
searchItem(input) {
this.search = input;

View File

@ -1,29 +1,17 @@
<template>
<div class="c-menu-button c-ctrl-wrapper c-ctrl-wrapper--menus-left">
<button
class="c-button--menu icon-notebook"
class="c-icon-button c-button--menu icon-camera"
title="Take a Notebook Snapshot"
@click="setNotebookTypes"
@click.stop="toggleMenu"
@click.stop.prevent="showMenu"
>
<span class="c-button__label"></span>
<span
title="Take Notebook Snapshot"
class="c-icon-button__label"
>
Snapshot
</span>
</button>
<div
v-show="showMenu"
class="c-menu"
>
<ul>
<li
v-for="(type, index) in notebookTypes"
:key="index"
:class="type.cssClass"
:title="type.name"
@click="snapshot(type)"
>
{{ type.name }}
</li>
</ul>
</div>
</div>
</template>
@ -57,22 +45,20 @@ export default {
data() {
return {
notebookSnapshot: null,
notebookTypes: [],
showMenu: false
notebookTypes: []
};
},
mounted() {
this.notebookSnapshot = new Snapshot(this.openmct);
document.addEventListener('click', this.hideMenu);
},
destroyed() {
document.removeEventListener('click', this.hideMenu);
this.setDefaultNotebookStatus();
},
methods: {
setNotebookTypes() {
showMenu(event) {
const notebookTypes = [];
const defaultNotebook = getDefaultNotebook();
const elementBoundingClientRect = this.$el.getBoundingClientRect();
const x = elementBoundingClientRect.x;
const y = elementBoundingClientRect.y + elementBoundingClientRect.height;
if (defaultNotebook) {
const domainObject = defaultNotebook.domainObject;
@ -83,28 +69,24 @@ export default {
notebookTypes.push({
cssClass: 'icon-notebook',
name: `Save to Notebook ${defaultPath}`,
type: NOTEBOOK_DEFAULT
callBack: () => {
return this.snapshot(NOTEBOOK_DEFAULT);
}
});
}
}
notebookTypes.push({
cssClass: 'icon-notebook',
cssClass: 'icon-camera',
name: 'Save to Notebook Snapshots',
type: NOTEBOOK_SNAPSHOT
callBack: () => {
return this.snapshot(NOTEBOOK_SNAPSHOT);
}
});
this.notebookTypes = notebookTypes;
},
toggleMenu() {
this.showMenu = !this.showMenu;
},
hideMenu() {
this.showMenu = false;
this.openmct.menus.showMenu(x, y, notebookTypes);
},
snapshot(notebook) {
this.hideMenu();
this.$nextTick(() => {
const element = document.querySelector('.c-overlay__contents')
|| document.getElementsByClassName('l-shell__main-container')[0];
@ -124,6 +106,15 @@ export default {
this.notebookSnapshot.capture(snapshotMeta, notebook.type, element);
});
},
setDefaultNotebookStatus() {
let defaultNotebookObject = getDefaultNotebook();
if (defaultNotebookObject && defaultNotebookObject.notebookMeta) {
let notebookIdentifier = defaultNotebookObject.notebookMeta.identifier;
this.openmct.status.set(notebookIdentifier, 'notebook-default');
}
}
}
};

View File

@ -4,7 +4,7 @@
<div class="l-browse-bar__start">
<div class="l-browse-bar__object-name--w">
<div class="l-browse-bar__object-name c-object-label">
<div class="c-object-label__type-icon icon-notebook"></div>
<div class="c-object-label__type-icon icon-camera"></div>
<div class="c-object-label__name">
Notebook Snapshots
<span v-if="snapshots.length"

View File

@ -1,5 +1,5 @@
<template>
<div class="c-indicator c-indicator--clickable icon-notebook"
<div class="c-indicator c-indicator--clickable icon-camera"
:class="[
{ 's-status-off': snapshotCount === 0 },
{ 's-status-on': snapshotCount > 0 },

View File

@ -1,3 +1,4 @@
import CopyToNotebookAction from './actions/CopyToNotebookAction';
import Notebook from './components/Notebook.vue';
import NotebookSnapshotIndicator from './components/NotebookSnapshotIndicator.vue';
import SnapshotContainer from './snapshot-container';
@ -13,6 +14,8 @@ export default function NotebookPlugin() {
installed = true;
openmct.actions.register(new CopyToNotebookAction(openmct));
const notebookType = {
name: 'Notebook',
description: 'Create and save timestamped notes with embedded object snapshots.',

View File

@ -20,7 +20,7 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
import { createOpenMct, createMouseEvent, resetApplicationState } from 'utils/testing';
import { createOpenMct, resetApplicationState } from 'utils/testing';
import NotebookPlugin from './plugin';
import Vue from 'vue';
@ -133,90 +133,4 @@ describe("Notebook plugin:", () => {
expect(hasMajorElements).toBe(true);
});
});
describe("Notebook Snapshots view:", () => {
let snapshotIndicator;
let drawerElement;
function clickSnapshotIndicator() {
const indicator = element.querySelector('.icon-notebook');
const button = indicator.querySelector('button');
const clickEvent = createMouseEvent('click');
button.dispatchEvent(clickEvent);
}
beforeAll(() => {
snapshotIndicator = openmct.indicators.indicatorObjects
.find(indicator => indicator.key === 'notebook-snapshot-indicator').element;
element.append(snapshotIndicator);
return Vue.nextTick();
});
afterAll(() => {
snapshotIndicator.remove();
if (drawerElement) {
drawerElement.remove();
}
});
beforeEach(() => {
drawerElement = document.querySelector('.l-shell__drawer');
});
afterEach(() => {
if (drawerElement) {
drawerElement.classList.remove('is-expanded');
}
});
it("has Snapshots indicator", () => {
const hasSnapshotIndicator = snapshotIndicator !== null && snapshotIndicator !== undefined;
expect(hasSnapshotIndicator).toBe(true);
});
it("snapshots container has class isExpanded", () => {
let classes = drawerElement.classList;
const isExpandedBefore = classes.contains('is-expanded');
clickSnapshotIndicator();
classes = drawerElement.classList;
const isExpandedAfterFirstClick = classes.contains('is-expanded');
const success = isExpandedBefore === false
&& isExpandedAfterFirstClick === true;
expect(success).toBe(true);
});
it("snapshots container does not have class isExpanded", () => {
let classes = drawerElement.classList;
const isExpandedBefore = classes.contains('is-expanded');
clickSnapshotIndicator();
classes = drawerElement.classList;
const isExpandedAfterFirstClick = classes.contains('is-expanded');
clickSnapshotIndicator();
classes = drawerElement.classList;
const isExpandedAfterSecondClick = classes.contains('is-expanded');
const success = isExpandedBefore === false
&& isExpandedAfterFirstClick === true
&& isExpandedAfterSecondClick === false;
expect(success).toBe(true);
});
it("show notebook snapshots container text", () => {
clickSnapshotIndicator();
const notebookSnapshots = drawerElement.querySelector('.l-browse-bar__object-name');
const snapshotsText = notebookSnapshots.textContent.trim();
expect(snapshotsText).toBe('Notebook Snapshots');
});
});
});

View File

@ -49,7 +49,7 @@ export default class Snapshot {
.then(domainObject => {
addNotebookEntry(this.openmct, domainObject, notebookStorage, embed);
const defaultPath = `${domainObject.name} > ${notebookStorage.section.name} > ${notebookStorage.page.name}`;
const defaultPath = `${domainObject.name} - ${notebookStorage.section.name} - ${notebookStorage.page.name}`;
const msg = `Saved to Notebook ${defaultPath}`;
this._showNotification(msg);
});

View File

@ -1,5 +1,6 @@
import objectLink from '../../../ui/mixins/object-link';
export const DEFAULT_CLASS = 'is-notebook-default';
export const DEFAULT_CLASS = 'notebook-default';
const TIME_BOUNDS = {
START_BOUND: 'tc.startBound',
END_BOUND: 'tc.endBound',
@ -96,7 +97,7 @@ export function createNewEmbed(snapshotMeta, snapshot = '') {
};
}
export function addNotebookEntry(openmct, domainObject, notebookStorage, embed = null) {
export function addNotebookEntry(openmct, domainObject, notebookStorage, embed = null, entryText = '') {
if (!openmct || !domainObject || !notebookStorage) {
return;
}
@ -117,14 +118,14 @@ export function addNotebookEntry(openmct, domainObject, notebookStorage, embed =
const entry = {
id,
createdOn: date,
text: '',
text: entryText,
embeds
};
const newEntries = addEntryIntoPage(notebookStorage, entries, entry);
addDefaultClass(domainObject);
mutateObject(openmct, domainObject, 'configuration.entries', newEntries);
addDefaultClass(domainObject, openmct);
openmct.objects.mutate(domainObject, 'configuration.entries', newEntries);
return id;
}
@ -197,13 +198,6 @@ export function mutateObject(openmct, object, key, value) {
openmct.objects.mutate(object, key, value);
}
function addDefaultClass(domainObject) {
const classList = domainObject.classList || [];
if (classList.includes(DEFAULT_CLASS)) {
return;
}
classList.push(DEFAULT_CLASS);
domainObject.classList = classList;
function addDefaultClass(domainObject, openmct) {
openmct.status.set(domainObject.identifier, DEFAULT_CLASS);
}

View File

@ -40,7 +40,7 @@
<div class="c-state-indicator__alert-cursor-lock icon-cursor-lock" title="Cursor is point locked. Click anywhere in the plot to unlock."></div>
<div class="plot-legend-item"
ng-class="{
'is-missing': series.domainObject.status === 'missing'
'is-status--missing': series.domainObject.status === 'missing'
}"
ng-repeat="series in series track by $index"
>
@ -48,7 +48,7 @@
<span class="plot-series-color-swatch"
ng-style="{ 'background-color': series.get('color').asHexString() }">
</span>
<span class="is-missing__indicator" title="This item is missing"></span>
<span class="is-status__indicator" title="This item is missing or suspect"></span>
<span class="plot-series-name">{{ series.nameWithUnit() }}</span>
</div>
<div class="plot-series-value hover-value-enabled value-to-display-{{ legend.get('valueToShowWhenCollapsed') }} {{ series.closest.mctLimitState.cssClass }}"
@ -95,14 +95,14 @@
<tr ng-repeat="series in series"
class="plot-legend-item"
ng-class="{
'is-missing': series.domainObject.status === 'missing'
'is-status--missing': series.domainObject.status === 'missing'
}"
>
<td class="plot-series-swatch-and-name">
<span class="plot-series-color-swatch"
ng-style="{ 'background-color': series.get('color').asHexString() }">
</span>
<span class="is-missing__indicator" title="This item is missing"></span>
<span class="is-status__indicator" title="This item is missing or suspect"></span>
<span class="plot-series-name">{{ series.get('name') }}</span>
</td>

View File

@ -58,7 +58,8 @@ define([
'./newFolderAction/plugin',
'./persistence/couch/plugin',
'./defaultRootName/plugin',
'./timeline/plugin'
'./timeline/plugin',
'./viewDatumAction/plugin'
], function (
_,
UTCTimeSystem,
@ -97,7 +98,8 @@ define([
NewFolderAction,
CouchDBPlugin,
DefaultRootName,
Timeline
Timeline,
ViewDatumAction
) {
const bundleMap = {
LocalStorage: 'platform/persistence/local',
@ -191,6 +193,7 @@ define([
plugins.ISOTimeFormat = ISOTimeFormat.default;
plugins.DefaultRootName = DefaultRootName.default;
plugins.Timeline = Timeline.default;
plugins.ViewDatumAction = ViewDatumAction.default;
return plugins;
});

View File

@ -25,6 +25,8 @@ export default class RemoveAction {
this.key = 'remove';
this.description = 'Remove this object from its containing object.';
this.cssClass = "icon-trash";
this.group = "action";
this.priority = 1;
this.openmct = openmct;
}
@ -103,6 +105,16 @@ export default class RemoveAction {
let parentType = parent && this.openmct.types.get(parent.type);
let child = objectPath[0];
let locked = child.locked ? child.locked : parent && parent.locked;
let isEditing = this.openmct.editor.isEditing();
if (isEditing) {
let currentItemInView = this.openmct.router.path[0];
let domainObject = objectPath[0];
if (this.openmct.objects.areIdsEqual(currentItemInView.identifier, domainObject.identifier)) {
return false;
}
}
if (locked) {
return false;

View File

@ -23,6 +23,6 @@ import RemoveAction from "./RemoveAction";
export default function () {
return function (openmct) {
openmct.contextMenu.registerAction(new RemoveAction(openmct));
openmct.actions.register(new RemoveAction(openmct));
};
}

View File

@ -20,8 +20,8 @@
Drag objects here to add them to this view.
</div>
<div
v-for="(tab,index) in tabsList"
:key="index"
v-for="(tab, index) in tabsList"
:key="tab.keyString"
class="c-tab c-tabs-view__tab"
:class="{
'is-current': isCurrent(tab)
@ -29,13 +29,13 @@
@click="showTab(tab, index)"
>
<div class="c-tabs-view__tab__label c-object-label"
:class="{'is-missing': tab.domainObject.status === 'missing'}"
:class="[tab.status ? `is-status--${tab.status}` : '']"
>
<div class="c-object-label__type-icon"
:class="tab.type.definition.cssClass"
>
<span class="is-missing__indicator"
title="This item is missing"
<span class="is-status__indicator"
:title="`This item is ${tab.status}`"
></span>
</div>
<span class="c-button__label c-object-label__name">{{ tab.domainObject.name }}</span>
@ -47,8 +47,8 @@
</div>
</div>
<div
v-for="(tab, index) in tabsList"
:key="index"
v-for="tab in tabsList"
:key="tab.keyString"
class="c-tabs-view__object-holder"
:class="{'c-tabs-view__object-holder--hidden': !isCurrent(tab)}"
>
@ -56,6 +56,7 @@
v-if="internalDomainObject.keep_alive ? currentTab : isCurrent(tab)"
class="c-tabs-view__object"
:object="tab.domainObject"
:object-path="tab.objectPath"
/>
</div>
</div>
@ -78,7 +79,7 @@ const unknownObjectType = {
};
export default {
inject: ['openmct', 'domainObject', 'composition'],
inject: ['openmct', 'domainObject', 'composition', 'objectPath'],
components: {
ObjectView
},
@ -139,6 +140,10 @@ export default {
this.composition.off('remove', this.removeItem);
this.composition.off('reorder', this.onReorder);
this.tabsList.forEach(tab => {
tab.statusUnsubscribe();
});
this.unsubscribe();
this.clearCurrentTabIndexFromURL();
@ -192,10 +197,19 @@ export default {
},
addItem(domainObject) {
let type = this.openmct.types.get(domainObject.type) || unknownObjectType;
let keyString = this.openmct.objects.makeKeyString(domainObject.identifier);
let status = this.openmct.status.get(domainObject.identifier);
let statusUnsubscribe = this.openmct.status.observe(keyString, (updatedStatus) => {
this.updateStatus(keyString, updatedStatus);
});
let objectPath = [domainObject].concat(this.objectPath.slice());
let tabItem = {
domainObject,
type: type,
key: this.openmct.objects.makeKeyString(domainObject.identifier)
status,
statusUnsubscribe,
objectPath,
type,
keyString
};
this.tabsList.push(tabItem);
@ -211,10 +225,12 @@ export default {
},
removeItem(identifier) {
let pos = this.tabsList.findIndex(tab =>
tab.domainObject.identifier.namespace === identifier.namespace && tab.domainObject.identifier.key === identifier.key
tab.domainObject.identifier.namespace === identifier.namespace && tab.domainObject.identifier.keyString === identifier.keyString
);
let tabToBeRemoved = this.tabsList[pos];
tabToBeRemoved.statusUnsubscribe();
this.tabsList.splice(pos, 1);
if (this.isCurrent(tabToBeRemoved)) {
@ -252,7 +268,7 @@ export default {
this.allowDrop = false;
},
isCurrent(tab) {
return this.currentTab.key === tab.key;
return this.currentTab.keyString === tab.keyString;
},
updateInternalDomainObject(domainObject) {
this.internalDomainObject = domainObject;
@ -270,6 +286,16 @@ export default {
},
clearCurrentTabIndexFromURL() {
deleteSearchParam(this.searchTabKey);
},
updateStatus(keyString, status) {
let tabPos = this.tabsList.findIndex((tab) => {
return tab.keyString === keyString;
});
if (tabPos !== -1) {
let tab = this.tabsList[tabPos];
this.$set(tab, 'status', status);
}
}
}
};

View File

@ -38,7 +38,7 @@ define([
canEdit: function (domainObject) {
return domainObject.type === 'tabs';
},
view: function (domainObject) {
view: function (domainObject, objectPath) {
let component;
return {
@ -56,6 +56,7 @@ define([
provide: {
openmct,
domainObject,
objectPath,
composition: openmct.composition.get(domainObject)
},
template: '<tabs-component :isEditing="isEditing"></tabs-component>'

View File

@ -180,7 +180,6 @@ define([
processHistoricalData(telemetryData, columnMap, keyString, limitEvaluator) {
let telemetryRows = telemetryData.map(datum => new TelemetryTableRow(datum, columnMap, keyString, limitEvaluator));
this.boundedRows.add(telemetryRows);
this.emit('historical-rows-processed');
}
/**

View File

@ -26,6 +26,7 @@ define([], function () {
this.columns = columns;
this.datum = createNormalizedDatum(datum, columns);
this.fullDatum = datum;
this.limitEvaluator = limitEvaluator;
this.objectKeyString = objectKeyString;
}
@ -87,7 +88,7 @@ define([], function () {
}
getContextMenuActions() {
return [];
return ['viewDatumAction'];
}
}

View File

@ -54,15 +54,13 @@ define([
view(domainObject, objectPath) {
let table = new TelemetryTable(domainObject, openmct);
let component;
let markingProp = {
enable: true,
useAlternateControlBar: false,
rowName: '',
rowNamePlural: ''
};
return {
const view = {
show: function (element, editMode) {
component = new Vue({
el: element,
@ -72,7 +70,8 @@ define([
data() {
return {
isEditing: editMode,
markingProp
markingProp,
view
};
},
provide: {
@ -80,7 +79,7 @@ define([
table,
objectPath
},
template: '<table-component :isEditing="isEditing" :marking="markingProp"/>'
template: '<table-component ref="tableComponent" :isEditing="isEditing" :marking="markingProp" :view="view"/>'
});
},
onEditModeChange(editMode) {
@ -89,11 +88,22 @@ define([
onClearData() {
table.clearData();
},
getViewContext() {
if (component) {
return component.$refs.tableComponent.getViewContext();
} else {
return {
type: 'telemetry-table'
};
}
},
destroy: function (element) {
component.$destroy();
component = undefined;
}
};
return view;
},
priority() {
return 1;

View File

@ -0,0 +1,123 @@
/*****************************************************************************
* 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.
*****************************************************************************/
let exportCSV = {
name: 'Export Table Data',
key: 'export-csv-all',
description: "Export this view's data",
cssClass: 'icon-download labeled',
invoke: (objectPath, viewProvider) => {
viewProvider.getViewContext().exportAllDataAsCSV();
},
group: 'view'
};
let exportMarkedRows = {
name: 'Export Marked Rows',
key: 'export-csv-marked',
description: "Export marked rows as CSV",
cssClass: 'icon-download labeled',
invoke: (objectPath, viewProvider) => {
viewProvider.getViewContext().exportMarkedRows();
},
group: 'view'
};
let unmarkAllRows = {
name: 'Unmark All Rows',
key: 'unmark-all-rows',
description: 'Unmark all rows',
cssClass: 'icon-x labeled',
invoke: (objectPath, viewProvider) => {
viewProvider.getViewContext().unmarkAllRows();
},
showInStatusBar: true,
group: 'view'
};
let pause = {
name: 'Pause',
key: 'pause-data',
description: 'Pause real-time data flow',
cssClass: 'icon-pause',
invoke: (objectPath, viewProvider) => {
viewProvider.getViewContext().togglePauseByButton();
},
showInStatusBar: true,
group: 'view'
};
let play = {
name: 'Play',
key: 'play-data',
description: 'Continue real-time data flow',
cssClass: 'c-button pause-play is-paused',
invoke: (objectPath, viewProvider) => {
viewProvider.getViewContext().togglePauseByButton();
},
showInStatusBar: true,
group: 'view'
};
let expandColumns = {
name: 'Expand Columns',
key: 'expand-columns',
description: "Increase column widths to fit currently available data.",
cssClass: 'icon-arrows-right-left labeled',
invoke: (objectPath, viewProvider) => {
viewProvider.getViewContext().expandColumns();
},
showInStatusBar: true,
group: 'view'
};
let autosizeColumns = {
name: 'Autosize Columns',
key: 'autosize-columns',
description: "Automatically size columns to fit the table into the available space.",
cssClass: 'icon-expand labeled',
invoke: (objectPath, viewProvider) => {
viewProvider.getViewContext().autosizeColumns();
},
showInStatusBar: true,
group: 'view'
};
let viewActions = [
exportCSV,
exportMarkedRows,
unmarkAllRows,
pause,
play,
expandColumns,
autosizeColumns
];
viewActions.forEach(action => {
action.appliesTo = (objectPath, viewProvider = {}) => {
let viewContext = viewProvider.getViewContext && viewProvider.getViewContext();
if (viewContext) {
let type = viewContext.type;
return type === 'telemetry-table';
}
return false;
};
});
export default viewActions;

View File

@ -102,7 +102,16 @@ export default {
selectable[columnKeys] = this.row.columns[columnKeys].selectable;
return selectable;
}, {})
}, {}),
actionsViewContext: {
getViewContext: () => {
return {
viewHistoricalData: true,
viewDatumAction: true,
getDatum: this.getDatum
};
}
}
};
},
computed: {
@ -170,14 +179,25 @@ export default {
event.stopPropagation();
}
},
getDatum() {
return this.row.fullDatum;
},
showContextMenu: function (event) {
event.preventDefault();
this.markRow(event);
this.row.getContextualDomainObject(this.openmct, this.row.objectKeyString).then(domainObject => {
let contextualObjectPath = this.objectPath.slice();
contextualObjectPath.unshift(domainObject);
this.openmct.contextMenu._showContextMenuForObjectPath(contextualObjectPath, event.x, event.y, this.row.getContextMenuActions());
let actionsCollection = this.openmct.actions.get(contextualObjectPath, this.actionsViewContext);
let allActions = actionsCollection.getActionsObject();
let applicableActions = this.row.getContextMenuActions().map(key => allActions[key]);
if (applicableActions.length) {
this.openmct.menus.showMenu(event.x, event.y, applicableActions);
}
});
}
}

View File

@ -23,8 +23,7 @@
<div class="c-table-wrapper"
:class="{ 'is-paused': paused }"
>
<!-- main contolbar start-->
<div v-if="!marking.useAlternateControlBar"
<div v-if="enableLegacyToolbar"
class="c-table-control-bar c-control-bar"
>
<button
@ -94,7 +93,6 @@
<slot name="buttons"></slot>
</div>
<!-- main controlbar end -->
<!-- alternate controlbar start -->
<div v-if="marking.useAlternateControlBar"
@ -113,11 +111,11 @@
<button
:class="{'hide-nice': !markedRows.length}"
class="c-button icon-x labeled"
class="c-icon-button icon-x labeled"
title="Deselect All"
@click="unmarkAllRows()"
>
<span class="c-button__label">{{ `Deselect ${marking.disableMultiSelect ? '' : 'All'}` }} </span>
<span class="c-icon-button__label">{{ `Deselect ${marking.disableMultiSelect ? '' : 'All'}` }} </span>
</button>
<slot name="buttons"></slot>
@ -301,12 +299,12 @@ export default {
default: true
},
allowFiltering: {
'type': Boolean,
'default': true
type: Boolean,
default: true
},
allowSorting: {
'type': Boolean,
'default': true
type: Boolean,
default: true
},
marking: {
type: Object,
@ -319,6 +317,17 @@ export default {
rowNamePlural: ""
};
}
},
enableLegacyToolbar: {
type: Boolean,
default: false
},
view: {
type: Object,
required: false,
default() {
return {};
}
}
},
data() {
@ -394,6 +403,40 @@ export default {
markedRows: {
handler(newVal, oldVal) {
this.$emit('marked-rows-updated', newVal, oldVal);
if (this.viewActionsCollection) {
if (newVal.length > 0) {
this.viewActionsCollection.enable(['export-csv-marked', 'unmark-all-rows']);
} else if (newVal.length === 0) {
this.viewActionsCollection.disable(['export-csv-marked', 'unmark-all-rows']);
}
}
}
},
paused: {
handler(newVal) {
if (this.viewActionsCollection) {
if (newVal) {
this.viewActionsCollection.hide(['pause-data']);
this.viewActionsCollection.show(['play-data']);
} else {
this.viewActionsCollection.hide(['play-data']);
this.viewActionsCollection.show(['pause-data']);
}
}
}
},
isAutosizeEnabled: {
handler(newVal) {
if (this.viewActionsCollection) {
if (newVal) {
this.viewActionsCollection.show(['expand-columns']);
this.viewActionsCollection.hide(['autosize-columns']);
} else {
this.viewActionsCollection.show(['autosize-columns']);
this.viewActionsCollection.hide(['expand-columns']);
}
}
}
}
},
@ -406,6 +449,11 @@ export default {
this.rowsRemoved = _.throttle(this.rowsRemoved, 200);
this.scroll = _.throttle(this.scroll, 100);
if (!this.marking.useAlternateControlBar && !this.enableLegacyToolbar) {
this.viewActionsCollection = this.openmct.actions.get(this.objectPath, this.view);
this.initializeViewActions();
}
this.table.on('object-added', this.addObject);
this.table.on('object-removed', this.removeObject);
this.table.on('outstanding-requests', this.outstandingRequests);
@ -846,7 +894,7 @@ export default {
for (let i = firstRowIndex; i <= lastRowIndex; i++) {
let row = allRows[i];
row.marked = true;
this.$set(row, 'marked', true);
if (row !== baseRow) {
this.markedRows.push(row);
@ -908,6 +956,40 @@ export default {
this.$nextTick().then(this.calculateColumnWidths);
},
getViewContext() {
return {
type: 'telemetry-table',
exportAllDataAsCSV: this.exportAllDataAsCSV,
exportMarkedRows: this.exportMarkedRows,
unmarkAllRows: this.unmarkAllRows,
togglePauseByButton: this.togglePauseByButton,
expandColumns: this.recalculateColumnWidths,
autosizeColumns: this.autosizeColumns
};
},
initializeViewActions() {
if (this.markedRows.length > 0) {
this.viewActionsCollection.enable(['export-csv-marked', 'unmark-all-rows']);
} else if (this.markedRows.length === 0) {
this.viewActionsCollection.disable(['export-csv-marked', 'unmark-all-rows']);
}
if (this.paused) {
this.viewActionsCollection.hide(['pause-data']);
this.viewActionsCollection.show(['play-data']);
} else {
this.viewActionsCollection.hide(['play-data']);
this.viewActionsCollection.show(['pause-data']);
}
if (this.isAutosizeEnabled) {
this.viewActionsCollection.show(['expand-columns']);
this.viewActionsCollection.hide(['autosize-columns']);
} else {
this.viewActionsCollection.show(['autosize-columns']);
this.viewActionsCollection.hide(['expand-columns']);
}
},
setRowHeight(height) {
this.rowHeight = height;
this.setHeight();

View File

@ -23,11 +23,13 @@
define([
'./TelemetryTableViewProvider',
'./TableConfigurationViewProvider',
'./TelemetryTableType'
'./TelemetryTableType',
'./ViewActions'
], function (
TelemetryTableViewProvider,
TableConfigurationViewProvider,
TelemetryTableType
TelemetryTableType,
TelemetryTableViewActions
) {
return function plugin() {
return function install(openmct) {
@ -41,6 +43,10 @@ define([
return true;
}
});
TelemetryTableViewActions.default.forEach(action => {
openmct.actions.register(action);
});
};
};
});

View File

@ -168,6 +168,8 @@ describe("the plugin", () => {
return telemetryPromise;
});
openmct.router.path = [testTelemetryObject];
applicableViews = openmct.objectViews.get(testTelemetryObject);
tableViewProvider = applicableViews.find((viewProvider) => viewProvider.key === 'table');
tableView = tableViewProvider.view(testTelemetryObject, [testTelemetryObject]);

View File

@ -0,0 +1,69 @@
/*****************************************************************************
* 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 MetadataListView from './components/MetadataList.vue';
import Vue from 'vue';
export default class ViewDatumAction {
constructor(openmct) {
this.name = 'View Full Datum';
this.key = 'viewDatumAction';
this.description = 'View full value of datum received';
this.cssClass = 'icon-object';
this._openmct = openmct;
}
invoke(objectPath, view) {
let viewContext = view.getViewContext && view.getViewContext();
let attributes = viewContext.getDatum && viewContext.getDatum();
let component = new Vue ({
provide: {
name: this.name,
attributes
},
components: {
MetadataListView
},
template: '<MetadataListView />'
});
this._openmct.overlays.overlay({
element: component.$mount().$el,
size: 'large',
dismissable: true,
onDestroy: () => {
component.$destroy();
}
});
}
appliesTo(objectPath, view = {}) {
let viewContext = (view.getViewContext && view.getViewContext()) || {};
let datum = viewContext.getDatum;
let enabled = viewContext.viewDatumAction;
if (enabled && datum) {
return true;
}
return false;
}
}

View File

@ -0,0 +1,24 @@
<template>
<div class="c-attributes-view">
<div class="c-overlay__top-bar">
<div class="c-overlay__dialog-title">{{ name }}</div>
</div>
<div class="c-overlay__contents-main l-preview-window__object-view">
<ul class="c-attributes-view__content">
<li
v-for="attribute in Object.keys(attributes)"
:key="attribute"
>
<span class="c-attributes-view__grid-item__label">{{ attribute }}</span>
<span class="c-attributes-view__grid-item__value">{{ attributes[attribute] }}</span>
</li>
</ul>
</div>
</div>
</template>
<script>
export default {
inject: ['name', 'attributes']
};
</script>

View File

@ -0,0 +1,30 @@
.c-attributes-view {
display: flex;
flex: 1 1 auto;
flex-direction: column;
> * {
flex: 0 0 auto;
}
&__content {
$p: 3px;
display: grid;
grid-template-columns: max-content 1fr;
grid-row-gap: $p;
li { display: contents; }
[class*="__grid-item"] {
border-bottom: 1px solid rgba(#999, 0.2);
padding: 0 5px $p 0;
}
[class*="__label"] {
opacity: 0.8;
}
}
}

View File

@ -0,0 +1,29 @@
/*****************************************************************************
* 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 ViewDatumAction from './ViewDatumAction.js';
export default function plugin() {
return function install(openmct) {
openmct.actions.register(new ViewDatumAction(openmct));
};
}

View File

@ -0,0 +1,95 @@
/*****************************************************************************
* 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 {
createOpenMct,
resetApplicationState
} from 'utils/testing';
describe("the plugin", () => {
let openmct;
let viewDatumAction;
let mockObjectPath;
let mockView;
let mockDatum;
beforeEach((done) => {
openmct = createOpenMct();
openmct.on('start', done);
openmct.startHeadless();
viewDatumAction = openmct.actions._allActions.viewDatumAction;
mockObjectPath = [{
name: 'mock object',
type: 'telemetry-table',
identifier: {
key: 'mock-object',
namespace: ''
}
}];
mockDatum = {
time: 123456789,
sin: 0.4455512,
cos: 0.4455512
};
mockView = {
getViewContext: () => {
return {
viewDatumAction: true,
getDatum: () => {
return mockDatum;
}
};
}
};
done();
});
afterEach(() => {
return resetApplicationState(openmct);
});
it('installs the view datum action', () => {
expect(viewDatumAction).toBeDefined();
});
describe('when invoked', () => {
beforeEach((done) => {
openmct.overlays.overlay = function (options) {};
spyOn(openmct.overlays, 'overlay');
viewDatumAction.invoke(mockObjectPath, mockView);
done();
});
it('uses an overlay to show user datum values', () => {
expect(openmct.overlays.overlay).toHaveBeenCalled();
});
});
});

View File

@ -218,7 +218,7 @@ $colorBtnActiveFg: $colorOkFg;
$colorBtnSelectedBg: $colorSelectedBg;
$colorBtnSelectedFg: $colorSelectedFg;
$colorClickIconButton: $colorKey;
$colorClickIconButtonBgHov: rgba($colorKey, 0.6);
$colorClickIconButtonBgHov: rgba($colorKey, 0.3);
$colorClickIconButtonFgHov: $colorKeyHov;
$colorDropHint: $colorKey;
$colorDropHintBg: pushBack($colorDropHint, 10%);
@ -378,6 +378,11 @@ $colorItemTreeVC: $colorDisclosureCtrl;
$colorItemTreeVCHover: $colorDisclosureCtrlHov;
$shdwItemTreeIcon: none;
// Layout frame controls
$frameControlsColorFg: white;
$frameControlsColorBg: $colorKey;
$frameControlsShdw: $shdwMenu;
// Images
$colorThumbHoverBg: $colorItemTreeHoverBg;

View File

@ -382,6 +382,11 @@ $colorItemTreeVC: $colorDisclosureCtrl;
$colorItemTreeVCHover: $colorDisclosureCtrlHov;
$shdwItemTreeIcon: none;
// Layout frame controls
$frameControlsColorFg: white;
$frameControlsColorBg: $colorKey;
$frameControlsShdw: $shdwMenu;
// Images
$colorThumbHoverBg: $colorItemTreeHoverBg;

View File

@ -80,13 +80,13 @@ $uiColor: #289fec; // Resize bars, splitter bars, etc.
$colorInteriorBorder: rgba($colorBodyFg, 0.2);
$colorA: $colorBodyFg;
$colorAHov: $colorKey;
$filterHov: brightness(0.8) contrast(2); // Tree, location items
$colorSelectedBg: rgba($colorKey, 0.2);
$filterHov: hue-rotate(-10deg) brightness(0.8) contrast(2); // Tree, location items
$colorSelectedBg: pushBack($colorKey, 40%);
$colorSelectedFg: pullForward($colorBodyFg, 10%);
// Object labels
$objectLabelTypeIconOpacity: 0.5;
$objectLabelNameFilter: brightness(0.5);
$objectLabelNameFilter: brightness(0.9);
// Layout
$shellMainPad: 4px 0;
@ -378,6 +378,11 @@ $colorItemTreeVC: $colorDisclosureCtrl;
$colorItemTreeVCHover: $colorDisclosureCtrlHov;
$shdwItemTreeIcon: none;
// Layout frame controls
$frameControlsColorFg: $colorClickIconButton;
$frameControlsColorBg: $colorMenuBg;
$frameControlsShdw: $shdwMenu;
// Images
$colorThumbHoverBg: $colorItemTreeHoverBg;

View File

@ -512,7 +512,7 @@ select {
&__section-hint {
$m: $interiorMargin;
margin: $m 0;
margin: 0 0 $m 0;
padding: $m nth($menuItemPad, 2) 0 nth($menuItemPad, 2);
opacity: 0.6;
@ -524,7 +524,7 @@ select {
$m: $interiorMargin;
border-top: 1px solid $colorInteriorBorder;
margin: $m 0;
padding: $m nth($menuItemPad, 2) 0 nth($menuItemPad, 2);
padding: 0 nth($menuItemPad, 2) 0 nth($menuItemPad, 2);
}
}

View File

@ -59,14 +59,8 @@ mct-plot {
}
/*********************** MISSING ITEM INDICATORS */
.is-missing__indicator {
display: none;
}
.is-missing {
@include isMissing();
.is-missing__indicator {
font-size: 0.8em;
}
.is-status__indicator {
font-size: 0.8em;
}
}

View File

@ -129,31 +129,21 @@
}
}
@mixin isMissing($absPos: false) {
@mixin isStatus($absPos: false) {
// Supports CSS classing as follows:
// is-status--missing, is-status--suspect, etc.
// Common styles to be applied to tree items, object labels, grid and list item views
//opacity: 0.7;
//pointer-events: none; // Don't think we can do this, as disables title hover on icon element
.is-missing__indicator {
display: none ;
.is-status__indicator {
display: block ; // Set to display: none in status.scss
text-shadow: $colorBodyBg 0 0 2px;
color: $colorAlert;
font-family: symbolsfont;
&:before {
content: $glyph-icon-alert-triangle;
}
}
@if $absPos {
.is-missing__indicator {
@if $absPos {
position: absolute;
z-index: 3;
}
}
&.is-missing .is-missing__indicator,
.is-missing .is-missing__indicator { display: block !important; }
}
@mixin bgDiagonalStripes($c: yellow, $a: 0.1, $d: 40px) {
@ -502,8 +492,8 @@
}
@mixin cClickIconButtonLayout() {
$pLR: 4px;
$pTB: 4px;
$pLR: 5px;
$pTB: 5px;
padding: $pTB $pLR;
&:before,
@ -522,6 +512,7 @@
@include cControl();
@include cClickIconButtonLayout();
background: none;
color: $colorClickIconButton;
box-shadow: none;
cursor: pointer;
transition: $transOut;
@ -530,7 +521,8 @@
@include hover() {
transition: $transIn;
background: $colorClickIconButtonBgHov;
color: $colorClickIconButtonFgHov;
//color: $colorClickIconButtonFgHov;
filter: $filterHov;
}
&[class*="--major"] {

View File

@ -198,3 +198,27 @@ tr {
.u-alert { @include uIndicator($colorAlert, $colorAlertFg, $glyph-icon-alert-triangle); }
.u-error { @include uIndicator($colorError, $colorErrorFg, $glyph-icon-alert-triangle); }
.is-status {
&__indicator {
display: none; // Default state; is set to block when within an actual is-status class
}
&--missing {
@include isStatus();
.is-status__indicator:before {
color: $colorAlert;
content: $glyph-icon-alert-triangle;
}
}
&--suspect {
@include isStatus();
.is-status__indicator:before {
color: $colorWarningLo;
content: $glyph-icon-alert-rect;
}
}
}

View File

@ -97,6 +97,7 @@ div.c-table {
}
}
.c-table-control-bar {
.c-icon-button,
.c-click-icon,
.c-button {
&__label {

View File

@ -213,7 +213,8 @@
}
}
.is-notebook-default {
.is-notebook-default,
.is-status--notebook-default {
&:after {
color: $colorFilter;
content: $glyph-icon-notebook-page;

View File

@ -27,6 +27,7 @@
@import "../plugins/timeConductor/conductor-mode-icon.scss";
@import "../plugins/timeConductor/date-picker.scss";
@import "../plugins/timeline/timeline-axis.scss";
@import "../plugins/viewDatumAction/components/metadata-list.scss";
@import "../ui/components/object-frame.scss";
@import "../ui/components/object-label.scss";
@import "../ui/components/progress-bar.scss";

View File

@ -21,45 +21,70 @@
*****************************************************************************/
<template>
<div
class="c-so-view has-local-controls"
:class="{
'c-so-view--no-frame': !hasFrame,
'has-complex-content': complexContent,
'is-missing': domainObject.status === 'missing'
}"
class="c-so-view"
:class="[
statusClass,
'c-so-view--' + domainObject.type,
{
'c-so-view--no-frame': !hasFrame,
'has-complex-content': complexContent
}
]"
>
<div class="c-so-view__header">
<div
class="c-so-view__header"
>
<div class="c-object-label"
:class="{
classList,
'is-missing': domainObject.status === 'missing'
}"
:class="[ statusClass ]"
>
<div class="c-object-label__type-icon"
:class="cssClass"
>
<span class="is-missing__indicator"
title="This item is missing"
<span class="is-status__indicator"
:title="`This item is ${status}`"
></span>
</div>
<div class="c-object-label__name">
{{ domainObject && domainObject.name }}
</div>
</div>
<context-menu-drop-down
:object-path="objectPath"
/>
<div
class="c-so-view__frame-controls"
:class="{
'c-so-view__frame-controls--no-frame': !hasFrame,
'has-complex-content': complexContent
}"
>
<div class="c-so-view__frame-controls__btns">
<button
v-for="(item, index) in statusBarItems"
:key="index"
class="c-icon-button"
:class="item.cssClass"
:title="item.name"
@click="item.callBack"
>
<span class="c-icon-button__label">{{ item.name }}</span>
</button>
<button
class="c-icon-button icon-items-expand"
title="View Large"
@click="expand"
>
<span class="c-icon-button__label">View Large</span>
</button>
</div>
<button
class="c-icon-button icon-3-dots c-so-view__frame-controls__more"
title="View menu items"
@click.prevent.stop="showMenuItems($event)"
></button>
</div>
</div>
<div class="c-so-view__local-controls c-so-view__view-large h-local-controls c-local-controls--show-on-hover">
<button
class="c-button icon-expand"
title="View Large"
@click="expand"
></button>
</div>
<div class="is-missing__indicator"
title="This item is missing"
></div>
<object-view
ref="objectView"
class="c-so-view__object-view"
@ -68,13 +93,13 @@
:object-path="objectPath"
:layout-font-size="layoutFontSize"
:layout-font="layoutFont"
@change-action-collection="setActionCollection"
/>
</div>
</template>
<script>
import ObjectView from './ObjectView.vue';
import ContextMenuDropDown from './contextMenuDropDown.vue';
import PreviewHeader from '@/ui/preview/preview-header.vue';
import Vue from 'vue';
@ -89,8 +114,7 @@ const SIMPLE_CONTENT_TYPES = [
export default {
inject: ['openmct'],
components: {
ObjectView,
ContextMenuDropDown
ObjectView
},
props: {
domainObject: {
@ -117,22 +141,32 @@ export default {
},
data() {
let objectType = this.openmct.types.get(this.domainObject.type);
let cssClass = objectType && objectType.definition ? objectType.definition.cssClass : 'icon-object-unknown';
let complexContent = !SIMPLE_CONTENT_TYPES.includes(this.domainObject.type);
return {
cssClass,
complexContent
complexContent,
statusBarItems: [],
status: ''
};
},
computed: {
classList() {
const classList = this.domainObject.classList;
if (!classList || !classList.length) {
return '';
}
statusClass() {
return (this.status) ? `is-status--${this.status}` : '';
}
},
mounted() {
this.status = this.openmct.status.get(this.domainObject.identifier);
this.removeStatusListener = this.openmct.status.observe(this.domainObject.identifier, this.setStatus);
},
beforeDestroy() {
this.removeStatusListener();
return classList.join(' ');
if (this.actionCollection) {
this.unlistenToActionCollection();
}
},
methods: {
@ -162,6 +196,7 @@ export default {
},
getPreviewHeader() {
const domainObject = this.objectPath[0];
const actionCollection = this.actionCollection;
const preview = new Vue({
components: {
PreviewHeader
@ -172,16 +207,41 @@ export default {
},
data() {
return {
domainObject
domainObject,
actionCollection
};
},
template: '<PreviewHeader :domainObject="domainObject" :hideViewSwitcher="true" :showNotebookMenuSwitcher="true"></PreviewHeader>'
template: '<PreviewHeader :actionCollection="actionCollection" :domainObject="domainObject" :hideViewSwitcher="true" :showNotebookMenuSwitcher="true"></PreviewHeader>'
});
return preview.$mount().$el;
},
getSelectionContext() {
return this.$refs.objectView.getSelectionContext();
},
setActionCollection(actionCollection) {
if (this.actionCollection) {
this.unlistenToActionCollection();
}
this.actionCollection = actionCollection;
this.actionCollection.on('update', this.updateActionItems);
this.updateActionItems(this.actionCollection.applicableActions);
},
unlistenToActionCollection() {
this.actionCollection.off('update', this.updateActionItems);
delete this.actionCollection;
},
updateActionItems(actionItems) {
this.statusBarItems = this.actionCollection.getStatusBarActions();
this.menuActionItems = this.actionCollection.getVisibleActions();
},
showMenuItems(event) {
let sortedActions = this.openmct.actions._groupAndSortActions(this.menuActionItems);
this.openmct.menus.showMenu(event.x, event.y, sortedActions);
},
setStatus(status) {
this.status = status;
}
}
};

View File

@ -1,10 +1,7 @@
<template>
<a
class="c-tree__item__label c-object-label"
:class="{
classList,
'is-missing': observedObject.status === 'missing'
}"
:class="[statusClass]"
draggable="true"
:href="objectLink"
@dragstart="dragStart"
@ -14,13 +11,11 @@
class="c-tree__item__type-icon c-object-label__type-icon"
:class="typeClass"
>
<span class="is-missing__indicator"
title="This item is missing"
<span class="is-status__indicator"
:title="`This item is ${status}`"
></span>
</div>
<div class="c-tree__item__name c-object-label__name"
:class="classList"
>
<div class="c-tree__item__name c-object-label__name">
{{ observedObject.name }}
</div>
</a>
@ -51,18 +46,11 @@ export default {
},
data() {
return {
observedObject: this.domainObject
observedObject: this.domainObject,
status: ''
};
},
computed: {
classList() {
const classList = this.observedObject.classList;
if (!classList || !classList.length) {
return '';
}
return classList.join(' ');
},
typeClass() {
let type = this.openmct.types.get(this.observedObject.type);
if (!type) {
@ -70,6 +58,9 @@ export default {
}
return type.definition.cssClass;
},
statusClass() {
return (this.status) ? `is-status--${this.status}` : '';
}
},
mounted() {
@ -80,8 +71,13 @@ export default {
this.$once('hook:destroyed', removeListener);
}
this.removeStatusListener = this.openmct.status.observe(this.observedObject.identifier, this.setStatus);
this.status = this.openmct.status.get(this.observedObject.identifier);
this.previewAction = new PreviewAction(this.openmct);
},
destroyed() {
this.removeStatusListener();
},
methods: {
navigateOrPreview(event) {
if (this.openmct.editor.isEditing()) {
@ -112,6 +108,9 @@ export default {
// (eg. notabook.)
event.dataTransfer.setData("openmct/domain-object-path", serializedPath);
event.dataTransfer.setData(`openmct/domain-object/${keyString}`, this.domainObject);
},
setStatus(status) {
this.status = status;
}
}
};

View File

@ -74,6 +74,11 @@ export default {
this.styleRuleManager.destroy();
delete this.styleRuleManager;
}
if (this.actionCollection) {
this.actionCollection.destroy();
delete this.actionCollection;
}
},
created() {
this.debounceUpdateView = _.debounce(this.updateView, 10);
@ -149,6 +154,7 @@ export default {
let keys = Object.keys(styleObj);
let elemToStyle = this.getStyleReceiver();
keys.forEach(key => {
if (elemToStyle) {
if ((typeof styleObj[key] === 'string') && (styleObj[key].indexOf('__no_value') > -1)) {
@ -207,6 +213,7 @@ export default {
}
}
this.getActionCollection();
this.currentView.show(this.viewContainer, this.openmct.editor.isEditing());
if (immediatelySelect) {
@ -216,6 +223,14 @@ export default {
this.openmct.objectViews.on('clearData', this.clearData);
},
getActionCollection() {
if (this.actionCollection) {
this.actionCollection.destroy();
}
this.actionCollection = this.openmct.actions._get(this.currentObjectPath || this.objectPath, this.currentView);
this.$emit('change-action-collection', this.actionCollection);
},
show(object, viewKey, immediatelySelect, currentObjectPath) {
this.updateStyle();

View File

@ -2,20 +2,21 @@
display: flex;
flex-direction: column;
&.is-missing {
border: $borderMissing;
}
/*************************** HEADER */
&__header {
flex: 0 0 auto;
display: flex;
font-size: 1.05em;
justify-content: space-between;
align-items: center;
margin-bottom: $interiorMarginSm;
padding: 1px 2px;
padding: 3px;
.c-object-label {
font-size: 1.05em;
&__type-icon {
opacity: $objectLabelTypeIconOpacity;
}
&__name {
filter: $objectLabelNameFilter;
}
@ -31,45 +32,118 @@
}
}
&--no-frame {
> .c-so-view__header {
display: none;
/*************************** FRAME CONTROLS */
&__frame-controls {
display: flex;
&__btns,
&__more {
flex: 0 0 auto;
}
&.is-missing {
@include isMissing($absPos: true);
.is-in-small-container &,
.c-fl-frame & {
[class*="__label"] {
// button labels
display: none;
.is-missing__indicator {
top: $interiorMargin;
left: $interiorMargin;
}
}
}
&__local-controls {
// View Large button
box-shadow: $colorLocalControlOvrBg 0 0 0 2px;
position: absolute;
top: $interiorMargin; right: $interiorMargin;
z-index: 10;
/*************************** HIDDEN FRAME */
&--no-frame {
> .c-so-view__header {
visibility: hidden;
pointer-events: none;
position: absolute;
top: 0; right: 0; bottom: auto; left: 0;
z-index: 2;
.c-object-label {
visibility: hidden;
}
.c-so-view__frame-controls {
background: $frameControlsColorBg;
border-radius: $controlCr;
box-shadow: $frameControlsShdw;
padding: 1px;
pointer-events: all;
.c-icon-button {
color: $frameControlsColorFg;
&:hover {
background: rgba($frameControlsColorFg, 0.3);
}
}
&__btns {
display: none;
}
&:hover {
[class*="__btns"] {
display: block;
}
}
[class*="__label"] {
// button labels
display: none;
}
}
}
&.c-so-view--layout {
// For sub-layouts with hidden frames, completely hide the header to avoid overlapping buttons
> .c-so-view__header {
display: none;
}
}
/* HOVERS */
&:hover {
> .c-so-view__header {
visibility: visible;
}
}
&[class*='is-status'] {
border: $borderMissing;
}
}
/*************************** OBJECT VIEW */
&__object-view {
flex: 1 1 auto;
height: 0; // Chrome 73 overflow bug fix
overflow: auto;
.u-fills-container {
// Expand component types that fill a container
@include abs();
}
}
.c-click-icon,
.c-button {
.c-button,
.c-icon-button {
// Shrink buttons a bit when they appear in a frame
border-radius: $smallCr !important;
font-size: 0.9em;
padding: 3px 5px;
}
&__view-large {
display: none;
padding: 5px;
}
&.has-complex-content {
> .c-so-view__view-large { display: block; }
}
&.is-status--missing {
border: $borderMissing;
}
&__object-view {
display: flex;
flex: 1 1 auto;
@ -86,4 +160,5 @@
// This element is the recipient for object styling; cannot be display: contents
flex: 1 1 auto;
overflow: hidden;
display: contents;
}

View File

@ -22,18 +22,20 @@
opacity: $objectLabelTypeIconOpacity;
}
&.is-missing {
@include isMissing($absPos: true);
.is-status__indicator {
position: absolute;
right: -3px;
top: -3px;
transform: scale(0.5);
}
[class*='__type-icon']:before,
[class*='__type-icon']:after{
opacity: $opacityMissing;
}
.is-missing__indicator {
right: -3px;
top: -3px;
transform: scale(0.7);
&[class*='is-status--missing'],
&[class*='is-status--suspect'] {
[class*='__type-icon'] {
&:before,
&:after {
opacity: $opacityMissing;
}
}
}
}

View File

@ -2,13 +2,13 @@
<div class="c-inspector__header">
<div v-if="!multiSelect"
class="c-inspector__selected c-object-label"
:class="{'is-missing': isMissing }"
:class="[statusClass]"
>
<div class="c-object-label__type-icon"
:class="typeCssClass"
>
<span class="is-missing__indicator"
title="This item is missing"
<span class="is-status__indicator"
:title="`This item is ${status}`"
></span>
</div>
<span v-if="!singleSelectNonObject"
@ -37,8 +37,10 @@ export default {
data() {
return {
domainObject: {},
keyString: undefined,
multiSelect: false,
itemsSelected: 0
itemsSelected: 0,
status: undefined
};
},
computed: {
@ -58,9 +60,8 @@ export default {
singleSelectNonObject() {
return !this.item.identifier && !this.multiSelect;
},
isMissing() {
// safe check this.domainObject since for layout objects this.domainOjbect is undefined
return this.domainObject && this.domainObject.status === 'missing';
statusClass() {
return this.status ? `is-status--${this.status}` : '';
}
},
mounted() {
@ -69,25 +70,45 @@ export default {
},
beforeDestroy() {
this.openmct.selection.off('change', this.updateSelection);
if (this.statusUnsubscribe) {
this.statusUnsubscribe();
}
},
methods: {
updateSelection(selection) {
if (this.statusUnsubscribe) {
this.statusUnsubscribe();
this.statusUnsubscribe = undefined;
}
if (selection.length === 0 || selection[0].length === 0) {
this.domainObject = {};
this.resetDomainObject();
return;
}
if (selection.length > 1) {
this.multiSelect = true;
this.domainObject = {};
this.itemsSelected = selection.length;
this.resetDomainObject();
return;
} else {
this.multiSelect = false;
this.domainObject = selection[0][0].context.item;
this.keyString = this.openmct.objects.makeKeyString(this.domainObject.identifier);
this.status = this.openmct.status.get(this.keyString);
this.statusUnsubscribe = this.openmct.status.observe(this.keyString, this.updateStatus);
}
},
resetDomainObject() {
this.domainObject = {};
this.status = undefined;
this.keyString = undefined;
},
updateStatus(status) {
this.status = status;
}
}
};

View File

@ -29,6 +29,10 @@
filter: $objectLabelNameFilter;
}
.c-object-label__type-icon {
opacity: $objectLabelTypeIconOpacity;
}
&--non-domain-object .c-object-label__name {
font-style: italic;
}

View File

@ -9,13 +9,13 @@
></button>
<div
class="l-browse-bar__object-name--w c-object-label"
:class="[classList, { 'is-missing': domainObject.status === 'missing' }]"
:class="[statusClass]"
>
<div class="c-object-label__type-icon"
:class="type.cssClass"
>
<span class="is-missing__indicator"
title="This item is missing"
<span class="is-status__indicator"
:title="`This item is ${status}`"
></span>
</div>
<span
@ -28,10 +28,6 @@
{{ domainObject.name }}
</span>
</div>
<div
class="l-browse-bar__context-actions c-disclosure-button"
@click.prevent.stop="showContextMenu"
></div>
</div>
<div class="l-browse-bar__end">
@ -39,7 +35,6 @@
v-if="!isEditing"
:current-view="currentView"
:views="views"
@setView="setView"
/>
<!-- Action buttons -->
<NotebookMenuSwitcher v-if="notebookEnabled"
@ -49,15 +44,24 @@
/>
<div class="l-browse-bar__actions">
<button
v-if="isViewEditable && !isEditing"
:title="lockedOrUnlocked"
v-for="(item, index) in statusBarItems"
:key="index"
class="c-button"
:class="item.cssClass"
@click="item.callBack"
>
</button>
<button
v-if="isViewEditable & !isEditing"
:title="lockedOrUnlockedTitle"
:class="{
'icon-lock c-button--caution': domainObject.locked,
'icon-unlocked': !domainObject.locked
'c-button icon-lock': domainObject.locked,
'c-icon-button icon-unlocked': !domainObject.locked
}"
@click="toggleLock(!domainObject.locked)"
></button>
<button
v-if="isViewEditable && !isEditing && !domainObject.locked"
class="l-browse-bar__actions__edit c-button c-button--major icon-pencil"
@ -103,6 +107,11 @@
title="Cancel Editing"
@click="promptUserandCancelEditing()"
></button>
<button
class="l-browse-bar__actions c-icon-button icon-3-dots"
title="More options"
@click.prevent.stop="showMenuItems($event)"
></button>
</div>
</div>
</div>
@ -120,6 +129,14 @@ export default {
NotebookMenuSwitcher,
ViewSwitcher
},
props: {
actionCollection: {
type: Object,
default: () => {
return {};
}
}
},
data: function () {
return {
notebookTypes: [],
@ -128,17 +145,14 @@ export default {
domainObject: PLACEHOLDER_OBJECT,
viewKey: undefined,
isEditing: this.openmct.editor.isEditing(),
notebookEnabled: this.openmct.types.get('notebook')
notebookEnabled: this.openmct.types.get('notebook'),
statusBarItems: [],
status: ''
};
},
computed: {
classList() {
const classList = this.domainObject.classList;
if (!classList || !classList.length) {
return '';
}
return classList.join(' ');
statusClass() {
return (this.status) ? `is-status--${this.status}` : '';
},
currentView() {
return this.views.filter(v => v.key === this.viewKey)[0] || {};
@ -152,7 +166,10 @@ export default {
return {
key: p.key,
cssClass: p.cssClass,
name: p.name
name: p.name,
callBack: () => {
return this.setView({key: p.key});
}
};
});
},
@ -184,7 +201,7 @@ export default {
return false;
},
lockedOrUnlocked() {
lockedOrUnlockedTitle() {
if (this.domainObject.locked) {
return 'Locked for editing - click to unlock.';
} else {
@ -201,6 +218,22 @@ export default {
this.mutationObserver = this.openmct.objects.observe(this.domainObject, '*', (domainObject) => {
this.domainObject = domainObject;
});
if (this.removeStatusListener) {
this.removeStatusListener();
}
this.status = this.openmct.status.get(this.domainObject.identifier, this.setStatus);
this.removeStatusListener = this.openmct.status.observe(this.domainObject.identifier, this.setStatus);
},
actionCollection(actionCollection) {
if (this.actionCollection) {
this.unlistenToActionCollection();
}
this.actionCollection = actionCollection;
this.actionCollection.on('update', this.updateActionItems);
this.updateActionItems(this.actionCollection.getActionsObject());
}
},
mounted: function () {
@ -216,6 +249,14 @@ export default {
this.mutationObserver();
}
if (this.actionCollection) {
this.unlistenToActionCollection();
}
if (this.removeStatusListener) {
this.removeStatusListener();
}
document.removeEventListener('click', this.closeViewAndSaveMenu);
window.removeEventListener('click', this.promptUserbeforeNavigatingAway);
},
@ -297,14 +338,26 @@ export default {
this.openmct.editor.edit();
});
},
showContextMenu(event) {
this.openmct.contextMenu._showContextMenuForObjectPath(this.openmct.router.path, event.clientX, event.clientY);
},
goToParent() {
window.location.hash = this.parentUrl;
},
updateActionItems(actionItems) {
this.statusBarItems = this.actionCollection.getStatusBarActions();
this.menuActionItems = this.actionCollection.getVisibleActions();
},
showMenuItems(event) {
let sortedActions = this.openmct.actions._groupAndSortActions(this.menuActionItems);
this.openmct.menus.showMenu(event.x, event.y, sortedActions);
},
unlistenToActionCollection() {
this.actionCollection.off('update', this.updateActionItems);
delete this.actionCollection;
},
toggleLock(flag) {
this.openmct.objects.mutate(this.domainObject, 'locked', flag);
},
setStatus(status) {
this.status = status;
}
}
};

View File

@ -70,6 +70,7 @@
<browse-bar
ref="browseBar"
class="l-shell__main-view-browse-bar"
:action-collection="actionCollection"
@sync-tree-navigation="handleSyncTreeNavigation"
/>
<toolbar
@ -79,8 +80,9 @@
<object-view
ref="browseObject"
class="l-shell__main-container"
:show-edit-view="true"
data-selectable
:show-edit-view="true"
@change-action-collection="setActionCollection"
/>
<component
:is="conductorComponent"
@ -144,8 +146,9 @@ export default {
conductorComponent: undefined,
isEditing: false,
hasToolbar: false,
headExpanded,
triggerSync: false
actionCollection: undefined,
triggerSync: false,
headExpanded
};
},
computed: {
@ -220,6 +223,9 @@ export default {
this.hasToolbar = structure.length > 0;
},
setActionCollection(actionCollection) {
this.actionCollection = actionCollection;
},
handleSyncTreeNavigation() {
this.triggerSync = !this.triggerSync;
}

View File

@ -4,36 +4,21 @@
class="l-browse-bar__view-switcher c-ctrl-wrapper c-ctrl-wrapper--menus-left"
>
<button
class="c-button--menu"
class="c-icon-button c-button--menu"
:class="currentView.cssClass"
title="Change the current view"
@click.stop="toggleViewMenu"
@click.prevent.stop="showMenu"
>
<span class="c-button__label">
<span class="c-icon-button__label">
{{ currentView.name }}
</span>
</button>
<div
v-show="showViewMenu"
class="c-menu"
>
<ul>
<li
v-for="(view, index) in views"
:key="index"
:class="view.cssClass"
:title="view.name"
@click="setView(view)"
>
{{ view.name }}
</li>
</ul>
</div>
</div>
</template>
<script>
export default {
inject: ['openmct'],
props: {
currentView: {
type: Object,
@ -44,26 +29,16 @@ export default {
required: true
}
},
data() {
return {
showViewMenu: false
};
},
mounted() {
document.addEventListener('click', this.hideViewMenu);
},
destroyed() {
document.removeEventListener('click', this.hideViewMenu);
},
methods: {
setView(view) {
this.$emit('setView', view);
},
toggleViewMenu() {
this.showViewMenu = !this.showViewMenu;
},
hideViewMenu() {
this.showViewMenu = false;
showMenu() {
const elementBoundingClientRect = this.$el.getBoundingClientRect();
const x = elementBoundingClientRect.x;
const y = elementBoundingClientRect.y + elementBoundingClientRect.height;
this.openmct.menus.showMenu(x, y, this.views);
}
}
};

View File

@ -333,7 +333,7 @@
}
> * + * {
margin-left: $interiorMargin;
margin-left: $interiorMarginSm;
}
}
@ -371,12 +371,16 @@
filter: $objectLabelNameFilter;
}
.c-object-label__type-icon {
opacity: $objectLabelTypeIconOpacity;
}
&__object-name--w {
@include headerFont(1.5em);
margin-left: $interiorMarginLg;
min-width: 0;
.is-missing__indicator {
.is-status__indicator {
right: -5px !important;
top: -4px !important;
}

View File

@ -30,7 +30,12 @@ export default {
showContextMenu(event) {
event.preventDefault();
event.stopPropagation();
this.openmct.contextMenu._showContextMenuForObjectPath(this.objectPath, event.clientX, event.clientY);
let actionsCollection = this.openmct.actions.get(this.objectPath);
let actions = actionsCollection.getVisibleActions();
let sortedActions = this.openmct.actions._groupAndSortActions(actions);
this.openmct.menus.showMenu(event.clientX, event.clientY, sortedActions);
}
}
};

View File

@ -23,9 +23,9 @@
<div class="l-preview-window">
<PreviewHeader
:current-view="currentView"
:action-collection="actionCollection"
:domain-object="domainObject"
:views="views"
@setView="setView"
/>
<div class="l-preview-window__object-view">
<div ref="objectView"></div>
@ -51,26 +51,23 @@ export default {
return {
domainObject: domainObject,
viewKey: undefined
viewKey: undefined,
views: [],
currentView: {},
actionCollection: undefined
};
},
computed: {
views() {
return this
.openmct
.objectViews
.get(this.domainObject);
},
currentView() {
return this.views.filter(v => v.key === this.viewKey)[0] || {};
}
},
mounted() {
let view = this.openmct.objectViews.get(this.domainObject)[0];
this.setView(view);
this.views = this.openmct.objectViews.get(this.domainObject).map((view) => {
view.callBack = () => {
return this.setView(view);
};
return view;
});
this.setView(this.views[0]);
},
destroyed() {
this.view.destroy();
beforeDestroy() {
if (this.stopListeningStyles) {
this.stopListeningStyles();
}
@ -79,6 +76,13 @@ export default {
this.styleRuleManager.destroy();
delete this.styleRuleManager;
}
if (this.actionCollection) {
this.actionCollection.destroy();
}
},
destroyed() {
this.view.destroy();
},
methods: {
clear() {
@ -91,17 +95,29 @@ export default {
delete this.viewContainer;
},
setView(view) {
this.clear();
if (this.viewKey === view.key) {
return;
}
this.clear();
this.viewKey = view.key;
this.currentView = view;
this.viewContainer = document.createElement('div');
this.viewContainer.classList.add('l-angular-ov-wrapper');
this.$refs.objectView.append(this.viewContainer);
this.view = this.currentView.view(this.domainObject, this.objectPath);
this.getActionsCollection();
this.view.show(this.viewContainer, false);
this.initObjectStyles();
},
getActionsCollection() {
if (this.actionCollection) {
this.actionCollection.destroy();
}
this.actionCollection = this.openmct.actions._get(this.objectPath, this.view);
},
initObjectStyles() {
if (!this.styleRuleManager) {
this.styleRuleManager = new StyleRuleManager((this.domainObject.configuration && this.domainObject.configuration.objectStyles), this.openmct, this.updateStyle.bind(this));
@ -124,8 +140,9 @@ export default {
}
let keys = Object.keys(styleObj);
let firstChild = this.$refs.objectView.querySelector(':first-child');
keys.forEach(key => {
let firstChild = this.$refs.objectView.querySelector(':first-child');
if (firstChild) {
if ((typeof styleObj[key] === 'string') && (styleObj[key].indexOf('__no_value') > -1)) {
if (firstChild.style[key]) {

View File

@ -27,10 +27,12 @@ export default class PreviewAction {
/**
* Metadata
*/
this.name = 'Preview';
this.name = 'View';
this.key = 'preview';
this.description = 'Preview in large dialog';
this.cssClass = 'icon-eye-open';
this.description = 'View in large dialog';
this.cssClass = 'icon-items-expand';
this.group = 'windowing';
this.priority = 1;
/**
* Dependencies
@ -81,8 +83,7 @@ export default class PreviewAction {
let targetObject = objectPath[0];
let navigatedObject = this._openmct.router.path[0];
return targetObject.identifier.namespace === navigatedObject.identifier.namespace
&& targetObject.identifier.key === navigatedObject.identifier.key;
return this._openmct.objects.areIdsEqual(targetObject.identifier, navigatedObject.identifier);
}
_preventPreview(objectPath) {
const noPreviewTypes = ['folder'];

View File

@ -32,4 +32,14 @@ export default class ViewHistoricalDataAction extends PreviewAction {
this.cssClass = 'icon-eye-open';
this.hideInDefaultMenu = true;
}
appliesTo(objectPath, view = {}) {
let viewContext = view.getViewContext && view.getViewContext();
if (objectPath.length && viewContext && viewContext.viewHistoricalData) {
return true;
} else {
return false;
}
}
}

View File

@ -24,7 +24,7 @@ import ViewHistoricalDataAction from './ViewHistoricalDataAction';
export default function () {
return function (openmct) {
openmct.contextMenu.registerAction(new PreviewAction(openmct));
openmct.contextMenu.registerAction(new ViewHistoricalDataAction(openmct));
openmct.actions.register(new PreviewAction(openmct));
openmct.actions.register(new ViewHistoricalDataAction(openmct));
};
}

View File

@ -13,13 +13,25 @@
</div>
</div>
<div class="l-browse-bar__end">
<view-switcher
:v-if="!hideViewSwitcher"
:views="views"
:current-view="currentView"
/>
<div class="l-browse-bar__actions">
<view-switcher
:v-if="!hideViewSwitcher"
:views="views"
:current-view="currentView"
@setView="setView"
/>
<button
v-for="(item, index) in statusBarItems"
:key="index"
class="c-button"
:class="item.cssClass"
@click="item.callBack"
>
</button>
<button
class="l-browse-bar__actions c-icon-button icon-3-dots"
title="More options"
@click.prevent.stop="showMenuItems($event)"
></button>
</div>
</div>
</div>
@ -27,6 +39,11 @@
<script>
import ViewSwitcher from '../../ui/layout/ViewSwitcher.vue';
const HIDDEN_ACTIONS = [
'remove',
'move',
'preview'
];
export default {
inject: [
@ -59,16 +76,53 @@ export default {
default: () => {
return [];
}
},
actionCollection: {
type: Object,
default: () => {
return undefined;
}
}
},
data() {
return {
type: this.openmct.types.get(this.domainObject.type)
type: this.openmct.types.get(this.domainObject.type),
statusBarItems: [],
menuActionItems: []
};
},
watch: {
actionCollection(actionCollection) {
if (this.actionCollection) {
this.unlistenToActionCollection();
}
this.actionCollection.on('update', this.updateActionItems);
this.updateActionItems(this.actionCollection.getActionsObject());
}
},
mounted() {
if (this.actionCollection) {
this.actionCollection.on('update', this.updateActionItems);
this.updateActionItems(this.actionCollection.getActionsObject());
}
},
methods: {
setView(view) {
this.$emit('setView', view);
},
unlistenToActionCollection() {
this.actionCollection.off('update', this.updateActionItems);
delete this.actionCollection;
},
updateActionItems() {
this.actionCollection.hide(HIDDEN_ACTIONS);
this.statusBarItems = this.actionCollection.getStatusBarActions();
this.menuActionItems = this.actionCollection.getVisibleActions();
},
showMenuItems(event) {
let sortedActions = this.openmct.actions._groupAndSortActions(this.menuActionItems);
this.openmct.menus.showMenu(event.x, event.y, sortedActions);
}
}
};

13
src/utils/clipboard.js Normal file
View File

@ -0,0 +1,13 @@
class Clipboard {
updateClipboard(newClip) {
// return promise
return navigator.clipboard.writeText(newClip);
}
readClipboard() {
// return promise
return navigator.clipboard.readText();
}
}
export default new Clipboard();