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" "$window"
], ],
"group": "windowing", "group": "windowing",
"cssClass": "icon-new-window", "priority": 10,
"priority": "preferred" "cssClass": "icon-new-window"
} }
], ],
"runs": [ "runs": [

View File

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

View File

@ -20,12 +20,12 @@
at runtime from the About dialog for additional information. at runtime from the About dialog for additional information.
--> -->
<div class="c-object-label" <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()}}" <div class="c-object-label__type-icon {{type.getCssClass()}}"
ng-class="{ 'l-icon-link':location.isLink() }" 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>
<div class='c-object-label__name'>{{model.name}}</div> <div class='c-object-label__name'>{{model.name}}</div>
</div> </div>

View File

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

View File

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

View File

@ -242,7 +242,11 @@ define([
this.overlays = new OverlayAPI.default(); 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(); this.router = new ApplicationRouter();
@ -271,6 +275,7 @@ define([
this.install(this.plugins.URLTimeSettingsSynchronizer()); this.install(this.plugins.URLTimeSettingsSynchronizer());
this.install(this.plugins.NotificationIndicator()); this.install(this.plugins.NotificationIndicator());
this.install(this.plugins.NewFolderAction()); this.install(this.plugins.NewFolderAction());
this.install(this.plugins.ViewDatumAction());
} }
MCT.prototype = Object.create(EventEmitter.prototype); MCT.prototype = Object.create(EventEmitter.prototype);

View File

@ -35,5 +35,5 @@ export default function LegacyActionAdapter(openmct, legacyActions) {
legacyActions.filter(contextualCategoryOnly) legacyActions.filter(contextualCategoryOnly)
.map(LegacyAction => new LegacyContextMenuAction(openmct, LegacyAction)) .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.description = LegacyAction.definition.description;
this.cssClass = LegacyAction.definition.cssClass; this.cssClass = LegacyAction.definition.cssClass;
this.LegacyAction = LegacyAction; this.LegacyAction = LegacyAction;
this.group = LegacyAction.definition.group;
this.priority = LegacyAction.definition.priority;
} }
invoke(objectPath) { 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', './telemetry/TelemetryAPI',
'./indicators/IndicatorAPI', './indicators/IndicatorAPI',
'./notifications/NotificationAPI', './notifications/NotificationAPI',
'./contextMenu/ContextMenuAPI', './Editor',
'./Editor' './menu/MenuAPI',
'./actions/ActionsAPI',
'./status/StatusAPI'
], function ( ], function (
TimeAPI, TimeAPI,
ObjectAPI, ObjectAPI,
@ -39,8 +40,10 @@ define([
TelemetryAPI, TelemetryAPI,
IndicatorAPI, IndicatorAPI,
NotificationAPI, NotificationAPI,
ContextMenuAPI, EditorAPI,
EditorAPI MenuAPI,
ActionsAPI,
StatusAPI
) { ) {
return { return {
TimeAPI: TimeAPI, TimeAPI: TimeAPI,
@ -51,6 +54,8 @@ define([
IndicatorAPI: IndicatorAPI, IndicatorAPI: IndicatorAPI,
NotificationAPI: NotificationAPI.default, NotificationAPI: NotificationAPI.default,
EditorAPI: EditorAPI, 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(); this.dismissLastOverlay();
} }
}); });
} }
/** /**
@ -127,6 +128,7 @@ class OverlayAPI {
return progressDialog; return progressDialog;
} }
} }
export default OverlayAPI; 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> <script>
const CONTEXT_MENU_ACTIONS = [ const CONTEXT_MENU_ACTIONS = [
'viewDatumAction',
'viewHistoricalData', 'viewHistoricalData',
'remove' 'remove'
]; ];
@ -129,6 +130,7 @@ export default {
let limit; let limit;
if (this.shouldUpdate(newTimestamp)) { if (this.shouldUpdate(newTimestamp)) {
this.datum = datum;
this.timestamp = newTimestamp; this.timestamp = newTimestamp;
this.value = this.formats[this.valueKey].format(datum); this.value = this.formats[this.valueKey].format(datum);
limit = this.limitEvaluator.evaluate(datum, this.valueMetadata); limit = this.limitEvaluator.evaluate(datum, this.valueMetadata);
@ -175,8 +177,25 @@ export default {
this.resetValues(); this.resetValues();
this.timestampKey = timeSystem.key; this.timestampKey = timeSystem.key;
}, },
getView() {
return {
getViewContext: () => {
return {
viewHistoricalData: true,
viewDatumAction: true,
getDatum: () => {
return this.datum;
}
};
}
};
},
showContextMenu(event) { 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() { resetValues() {
this.value = '---'; this.value = '---';

View File

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

View File

@ -53,7 +53,7 @@ define([
openmct.indicators.add(indicator); 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 () { describe('When the Clear Data Plugin is installed,', function () {
const mockObjectViews = jasmine.createSpyObj('objectViews', ['emit']); const mockObjectViews = jasmine.createSpyObj('objectViews', ['emit']);
const mockIndicatorProvider = jasmine.createSpyObj('indicators', ['add']); const mockIndicatorProvider = jasmine.createSpyObj('indicators', ['add']);
const mockContextMenuProvider = jasmine.createSpyObj('contextMenu', ['registerAction']); const mockActionsProvider = jasmine.createSpyObj('actions', ['register']);
const openmct = { const openmct = {
objectViews: mockObjectViews, objectViews: mockObjectViews,
indicators: mockIndicatorProvider, indicators: mockIndicatorProvider,
contextMenu: mockContextMenuProvider, actions: mockActionsProvider,
install: function (plugin) { install: function (plugin) {
plugin(this); plugin(this);
} }
@ -51,7 +51,7 @@ describe('When the Clear Data Plugin is installed,', function () {
it('Clear Data context menu action is installed', function () { it('Clear Data context menu action is installed', function () {
openmct.install(ClearDataActionPlugin([])); openmct.install(ClearDataActionPlugin([]));
expect(mockContextMenuProvider.registerAction).toHaveBeenCalled(); expect(mockActionsProvider.register).toHaveBeenCalled();
}); });
it('clear data action emits a clearData event when invoked', function () { it('clear data action emits a clearData event when invoked', function () {

View File

@ -64,9 +64,16 @@ define([
components: { components: {
AlphanumericFormatView: AlphanumericFormatView.default 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 () { destroy: function () {
component.$destroy(); component.$destroy();
component = undefined; 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,6 +142,9 @@ export default {
this.domainObject = domainObject; this.domainObject = domainObject;
this.currentObjectPath = [this.domainObject].concat(this.objectPath.slice()); this.currentObjectPath = [this.domainObject].concat(this.objectPath.slice());
this.$nextTick(() => { this.$nextTick(() => {
let reference = this.$refs.objectFrame;
if (reference) {
let childContext = this.$refs.objectFrame.getSelectionContext(); let childContext = this.$refs.objectFrame.getSelectionContext();
childContext.item = domainObject; childContext.item = domainObject;
childContext.layoutItem = this.item; childContext.layoutItem = this.item;
@ -150,6 +153,7 @@ export default {
this.removeSelectable = this.openmct.selection.selectable( this.removeSelectable = this.openmct.selection.selectable(
this.$el, this.context, this.immediatelySelect || this.initSelect); this.$el, this.context, this.immediatelySelect || this.initSelect);
delete this.immediatelySelect; delete this.immediatelySelect;
}
}); });
} }
} }

View File

@ -30,18 +30,15 @@
> >
<div <div
v-if="domainObject" v-if="domainObject"
class="u-style-receiver c-telemetry-view" class="c-telemetry-view"
:class="{ :class="[statusClass]"
styleClass,
'is-missing': domainObject.status === 'missing'
}"
:style="styleObject" :style="styleObject"
:data-font-size="item.fontSize" :data-font-size="item.fontSize"
:data-font="item.font" :data-font="item.font"
@contextmenu.prevent="showContextMenu" @contextmenu.prevent="showContextMenu"
> >
<div class="is-missing__indicator" <div class="is-status__indicator"
title="This item is missing" :title="`This item is ${status}`"
></div> ></div>
<div <div
v-if="showLabel" v-if="showLabel"
@ -76,10 +73,11 @@
import LayoutFrame from './LayoutFrame.vue'; import LayoutFrame from './LayoutFrame.vue';
import printj from 'printj'; import printj from 'printj';
import conditionalStylesMixin from "../mixins/objectStyles-mixin"; import conditionalStylesMixin from "../mixins/objectStyles-mixin";
import { getDefaultNotebook } from '@/plugins/notebook/utils/notebook-storage.js';
const DEFAULT_TELEMETRY_DIMENSIONS = [10, 5]; const DEFAULT_TELEMETRY_DIMENSIONS = [10, 5];
const DEFAULT_POSITION = [1, 1]; const DEFAULT_POSITION = [1, 1];
const CONTEXT_MENU_ACTIONS = ['viewHistoricalData']; const CONTEXT_MENU_ACTIONS = ['copyToClipboard', 'copyToNotebook', 'viewHistoricalData'];
export default { export default {
makeDefinition(openmct, gridSize, domainObject, position) { makeDefinition(openmct, gridSize, domainObject, position) {
@ -129,13 +127,18 @@ export default {
}, },
data() { data() {
return { return {
currentObjectPath: undefined,
datum: undefined, datum: undefined,
formats: undefined,
domainObject: undefined, domainObject: undefined,
currentObjectPath: undefined formats: undefined,
viewKey: `alphanumeric-format-${Math.random()}`,
status: ''
}; };
}, },
computed: { computed: {
statusClass() {
return (this.status) ? `is-status--${this.status}` : '';
},
showLabel() { showLabel() {
let displayMode = this.item.displayMode; let displayMode = this.item.displayMode;
@ -213,9 +216,13 @@ export default {
this.openmct.objects.get(this.item.identifier) this.openmct.objects.get(this.item.identifier)
.then(this.setObject); .then(this.setObject);
this.openmct.time.on("bounds", this.refreshData); 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() { destroyed() {
this.removeSubscription(); this.removeSubscription();
this.removeStatusListener();
if (this.removeSelectable) { if (this.removeSelectable) {
this.removeSelectable(); this.removeSelectable();
@ -224,6 +231,12 @@ export default {
this.openmct.time.off("bounds", this.refreshData); this.openmct.time.off("bounds", this.refreshData);
}, },
methods: { 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() { requestHistoricalData() {
let bounds = this.openmct.time.bounds(); let bounds = this.openmct.time.bounds();
let options = { let options = {
@ -261,6 +274,16 @@ export default {
this.requestHistoricalData(this.domainObject); this.requestHistoricalData(this.domainObject);
} }
}, },
getView() {
return {
getViewContext: () => {
return {
viewHistoricalData: true,
formattedValueForCopy: this.formattedValueForCopy
};
}
};
},
setObject(domainObject) { setObject(domainObject) {
this.domainObject = domainObject; this.domainObject = domainObject;
this.keyString = this.openmct.objects.makeKeyString(domainObject.identifier); this.keyString = this.openmct.objects.makeKeyString(domainObject.identifier);
@ -288,8 +311,33 @@ export default {
updateTelemetryFormat(format) { updateTelemetryFormat(format) {
this.$emit('formatChanged', this.item, format); this.$emit('formatChanged', this.item, format);
}, },
showContextMenu(event) { async getContextMenuActions() {
this.openmct.contextMenu._showContextMenuForObjectPath(this.currentObjectPath, event.x, event.y, CONTEXT_MENU_ACTIONS); 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; flex: 1 1 auto;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
// justify-content: center;
align-items: center; align-items: center;
overflow: hidden; overflow: hidden;
padding: $interiorMargin; padding: $interiorMargin;
@ -27,14 +26,13 @@
border: 1px solid transparent; border: 1px solid transparent;
} }
@include isMissing($absPos: true); .is-status__indicator {
position: absolute;
.is-missing__indicator {
top: 0; top: 0;
left: 0; left: 0;
} }
&.is-missing { &[class*='is-status'] {
border: $borderMissing; border: $borderMissing;
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -28,9 +28,5 @@
padding-top: $p; padding-top: $p;
padding-bottom: $p; padding-bottom: $p;
width: 25%; 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.name = 'Go To Original';
this.key = 'goToOriginal'; this.key = 'goToOriginal';
this.description = 'Go to the original unlinked instance of this object'; this.description = 'Go to the original unlinked instance of this object';
this.group = 'action';
this.priority = 4;
this._openmct = openmct; this._openmct = openmct;
} }

View File

@ -23,6 +23,6 @@ import GoToOriginalAction from './goToOriginalAction';
export default function () { export default function () {
return function (openmct) { 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.key = 'newFolder';
this.description = 'Create a new folder'; this.description = 'Create a new folder';
this.cssClass = 'icon-folder-new'; this.cssClass = 'icon-folder-new';
this.group = "action";
this.priority = 9;
this._openmct = openmct; this._openmct = openmct;
this._dialogForm = { this._dialogForm = {

View File

@ -23,6 +23,6 @@ import NewFolderAction from './newFolderAction';
export default function () { export default function () {
return function (openmct) { 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.on('start', done);
openmct.startHeadless(); openmct.startHeadless();
newFolderAction = openmct.contextMenu._allActions.filter(action => { newFolderAction = openmct.actions._allActions.newFolder;
return action.key === 'newFolder';
})[0];
}); });
afterEach(() => { 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 SearchResults from './SearchResults.vue';
import Sidebar from './Sidebar.vue'; import Sidebar from './Sidebar.vue';
import { clearDefaultNotebook, getDefaultNotebook, setDefaultNotebook, setDefaultNotebookSection, setDefaultNotebookPage } from '../utils/notebook-storage'; 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 objectUtils from 'objectUtils';
import { throttle } from 'lodash'; import { throttle } from 'lodash';
@ -431,14 +431,7 @@ export default {
return; return;
} }
const classList = domainObject.classList || []; this.openmct.status.delete(domainObject.identifier);
const index = classList.indexOf(DEFAULT_CLASS);
if (!classList.length || index < 0) {
return;
}
classList.splice(index, 1);
mutateObject(this.openmct, domainObject, 'classList', classList);
}, },
searchItem(input) { searchItem(input) {
this.search = input; this.search = input;

View File

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

View File

@ -1,5 +1,5 @@
<template> <template>
<div class="c-indicator c-indicator--clickable icon-notebook" <div class="c-indicator c-indicator--clickable icon-camera"
:class="[ :class="[
{ 's-status-off': snapshotCount === 0 }, { 's-status-off': snapshotCount === 0 },
{ 's-status-on': 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 Notebook from './components/Notebook.vue';
import NotebookSnapshotIndicator from './components/NotebookSnapshotIndicator.vue'; import NotebookSnapshotIndicator from './components/NotebookSnapshotIndicator.vue';
import SnapshotContainer from './snapshot-container'; import SnapshotContainer from './snapshot-container';
@ -13,6 +14,8 @@ export default function NotebookPlugin() {
installed = true; installed = true;
openmct.actions.register(new CopyToNotebookAction(openmct));
const notebookType = { const notebookType = {
name: 'Notebook', name: 'Notebook',
description: 'Create and save timestamped notes with embedded object snapshots.', 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. * 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 NotebookPlugin from './plugin';
import Vue from 'vue'; import Vue from 'vue';
@ -133,90 +133,4 @@ describe("Notebook plugin:", () => {
expect(hasMajorElements).toBe(true); 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 => { .then(domainObject => {
addNotebookEntry(this.openmct, domainObject, notebookStorage, embed); 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}`; const msg = `Saved to Notebook ${defaultPath}`;
this._showNotification(msg); this._showNotification(msg);
}); });

View File

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

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="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" <div class="plot-legend-item"
ng-class="{ ng-class="{
'is-missing': series.domainObject.status === 'missing' 'is-status--missing': series.domainObject.status === 'missing'
}" }"
ng-repeat="series in series track by $index" ng-repeat="series in series track by $index"
> >
@ -48,7 +48,7 @@
<span class="plot-series-color-swatch" <span class="plot-series-color-swatch"
ng-style="{ 'background-color': series.get('color').asHexString() }"> ng-style="{ 'background-color': series.get('color').asHexString() }">
</span> </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> <span class="plot-series-name">{{ series.nameWithUnit() }}</span>
</div> </div>
<div class="plot-series-value hover-value-enabled value-to-display-{{ legend.get('valueToShowWhenCollapsed') }} {{ series.closest.mctLimitState.cssClass }}" <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" <tr ng-repeat="series in series"
class="plot-legend-item" class="plot-legend-item"
ng-class="{ ng-class="{
'is-missing': series.domainObject.status === 'missing' 'is-status--missing': series.domainObject.status === 'missing'
}" }"
> >
<td class="plot-series-swatch-and-name"> <td class="plot-series-swatch-and-name">
<span class="plot-series-color-swatch" <span class="plot-series-color-swatch"
ng-style="{ 'background-color': series.get('color').asHexString() }"> ng-style="{ 'background-color': series.get('color').asHexString() }">
</span> </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> <span class="plot-series-name">{{ series.get('name') }}</span>
</td> </td>

View File

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

View File

@ -25,6 +25,8 @@ export default class RemoveAction {
this.key = 'remove'; this.key = 'remove';
this.description = 'Remove this object from its containing object.'; this.description = 'Remove this object from its containing object.';
this.cssClass = "icon-trash"; this.cssClass = "icon-trash";
this.group = "action";
this.priority = 1;
this.openmct = openmct; this.openmct = openmct;
} }
@ -103,6 +105,16 @@ export default class RemoveAction {
let parentType = parent && this.openmct.types.get(parent.type); let parentType = parent && this.openmct.types.get(parent.type);
let child = objectPath[0]; let child = objectPath[0];
let locked = child.locked ? child.locked : parent && parent.locked; 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) { if (locked) {
return false; return false;

View File

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

View File

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

View File

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

View File

@ -54,15 +54,13 @@ define([
view(domainObject, objectPath) { view(domainObject, objectPath) {
let table = new TelemetryTable(domainObject, openmct); let table = new TelemetryTable(domainObject, openmct);
let component; let component;
let markingProp = { let markingProp = {
enable: true, enable: true,
useAlternateControlBar: false, useAlternateControlBar: false,
rowName: '', rowName: '',
rowNamePlural: '' rowNamePlural: ''
}; };
const view = {
return {
show: function (element, editMode) { show: function (element, editMode) {
component = new Vue({ component = new Vue({
el: element, el: element,
@ -72,7 +70,8 @@ define([
data() { data() {
return { return {
isEditing: editMode, isEditing: editMode,
markingProp markingProp,
view
}; };
}, },
provide: { provide: {
@ -80,7 +79,7 @@ define([
table, table,
objectPath objectPath
}, },
template: '<table-component :isEditing="isEditing" :marking="markingProp"/>' template: '<table-component ref="tableComponent" :isEditing="isEditing" :marking="markingProp" :view="view"/>'
}); });
}, },
onEditModeChange(editMode) { onEditModeChange(editMode) {
@ -89,11 +88,22 @@ define([
onClearData() { onClearData() {
table.clearData(); table.clearData();
}, },
getViewContext() {
if (component) {
return component.$refs.tableComponent.getViewContext();
} else {
return {
type: 'telemetry-table'
};
}
},
destroy: function (element) { destroy: function (element) {
component.$destroy(); component.$destroy();
component = undefined; component = undefined;
} }
}; };
return view;
}, },
priority() { priority() {
return 1; 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; selectable[columnKeys] = this.row.columns[columnKeys].selectable;
return selectable; return selectable;
}, {}) }, {}),
actionsViewContext: {
getViewContext: () => {
return {
viewHistoricalData: true,
viewDatumAction: true,
getDatum: this.getDatum
};
}
}
}; };
}, },
computed: { computed: {
@ -170,14 +179,25 @@ export default {
event.stopPropagation(); event.stopPropagation();
} }
}, },
getDatum() {
return this.row.fullDatum;
},
showContextMenu: function (event) { showContextMenu: function (event) {
event.preventDefault(); event.preventDefault();
this.markRow(event);
this.row.getContextualDomainObject(this.openmct, this.row.objectKeyString).then(domainObject => { this.row.getContextualDomainObject(this.openmct, this.row.objectKeyString).then(domainObject => {
let contextualObjectPath = this.objectPath.slice(); let contextualObjectPath = this.objectPath.slice();
contextualObjectPath.unshift(domainObject); 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" <div class="c-table-wrapper"
:class="{ 'is-paused': paused }" :class="{ 'is-paused': paused }"
> >
<!-- main contolbar start--> <div v-if="enableLegacyToolbar"
<div v-if="!marking.useAlternateControlBar"
class="c-table-control-bar c-control-bar" class="c-table-control-bar c-control-bar"
> >
<button <button
@ -94,7 +93,6 @@
<slot name="buttons"></slot> <slot name="buttons"></slot>
</div> </div>
<!-- main controlbar end -->
<!-- alternate controlbar start --> <!-- alternate controlbar start -->
<div v-if="marking.useAlternateControlBar" <div v-if="marking.useAlternateControlBar"
@ -113,11 +111,11 @@
<button <button
:class="{'hide-nice': !markedRows.length}" :class="{'hide-nice': !markedRows.length}"
class="c-button icon-x labeled" class="c-icon-button icon-x labeled"
title="Deselect All" title="Deselect All"
@click="unmarkAllRows()" @click="unmarkAllRows()"
> >
<span class="c-button__label">{{ `Deselect ${marking.disableMultiSelect ? '' : 'All'}` }} </span> <span class="c-icon-button__label">{{ `Deselect ${marking.disableMultiSelect ? '' : 'All'}` }} </span>
</button> </button>
<slot name="buttons"></slot> <slot name="buttons"></slot>
@ -301,12 +299,12 @@ export default {
default: true default: true
}, },
allowFiltering: { allowFiltering: {
'type': Boolean, type: Boolean,
'default': true default: true
}, },
allowSorting: { allowSorting: {
'type': Boolean, type: Boolean,
'default': true default: true
}, },
marking: { marking: {
type: Object, type: Object,
@ -319,6 +317,17 @@ export default {
rowNamePlural: "" rowNamePlural: ""
}; };
} }
},
enableLegacyToolbar: {
type: Boolean,
default: false
},
view: {
type: Object,
required: false,
default() {
return {};
}
} }
}, },
data() { data() {
@ -394,6 +403,40 @@ export default {
markedRows: { markedRows: {
handler(newVal, oldVal) { handler(newVal, oldVal) {
this.$emit('marked-rows-updated', 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.rowsRemoved = _.throttle(this.rowsRemoved, 200);
this.scroll = _.throttle(this.scroll, 100); 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-added', this.addObject);
this.table.on('object-removed', this.removeObject); this.table.on('object-removed', this.removeObject);
this.table.on('outstanding-requests', this.outstandingRequests); this.table.on('outstanding-requests', this.outstandingRequests);
@ -846,7 +894,7 @@ export default {
for (let i = firstRowIndex; i <= lastRowIndex; i++) { for (let i = firstRowIndex; i <= lastRowIndex; i++) {
let row = allRows[i]; let row = allRows[i];
row.marked = true; this.$set(row, 'marked', true);
if (row !== baseRow) { if (row !== baseRow) {
this.markedRows.push(row); this.markedRows.push(row);
@ -908,6 +956,40 @@ export default {
this.$nextTick().then(this.calculateColumnWidths); 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) { setRowHeight(height) {
this.rowHeight = height; this.rowHeight = height;
this.setHeight(); this.setHeight();

View File

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

View File

@ -168,6 +168,8 @@ describe("the plugin", () => {
return telemetryPromise; return telemetryPromise;
}); });
openmct.router.path = [testTelemetryObject];
applicableViews = openmct.objectViews.get(testTelemetryObject); applicableViews = openmct.objectViews.get(testTelemetryObject);
tableViewProvider = applicableViews.find((viewProvider) => viewProvider.key === 'table'); tableViewProvider = applicableViews.find((viewProvider) => viewProvider.key === 'table');
tableView = tableViewProvider.view(testTelemetryObject, [testTelemetryObject]); 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; $colorBtnSelectedBg: $colorSelectedBg;
$colorBtnSelectedFg: $colorSelectedFg; $colorBtnSelectedFg: $colorSelectedFg;
$colorClickIconButton: $colorKey; $colorClickIconButton: $colorKey;
$colorClickIconButtonBgHov: rgba($colorKey, 0.6); $colorClickIconButtonBgHov: rgba($colorKey, 0.3);
$colorClickIconButtonFgHov: $colorKeyHov; $colorClickIconButtonFgHov: $colorKeyHov;
$colorDropHint: $colorKey; $colorDropHint: $colorKey;
$colorDropHintBg: pushBack($colorDropHint, 10%); $colorDropHintBg: pushBack($colorDropHint, 10%);
@ -378,6 +378,11 @@ $colorItemTreeVC: $colorDisclosureCtrl;
$colorItemTreeVCHover: $colorDisclosureCtrlHov; $colorItemTreeVCHover: $colorDisclosureCtrlHov;
$shdwItemTreeIcon: none; $shdwItemTreeIcon: none;
// Layout frame controls
$frameControlsColorFg: white;
$frameControlsColorBg: $colorKey;
$frameControlsShdw: $shdwMenu;
// Images // Images
$colorThumbHoverBg: $colorItemTreeHoverBg; $colorThumbHoverBg: $colorItemTreeHoverBg;

View File

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

View File

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

View File

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

View File

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

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 // 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 { .is-status__indicator {
display: none ; display: block ; // Set to display: none in status.scss
text-shadow: $colorBodyBg 0 0 2px; text-shadow: $colorBodyBg 0 0 2px;
color: $colorAlert;
font-family: symbolsfont; font-family: symbolsfont;
&:before {
content: $glyph-icon-alert-triangle;
}
}
@if $absPos { @if $absPos {
.is-missing__indicator {
position: absolute; position: absolute;
z-index: 3; 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) { @mixin bgDiagonalStripes($c: yellow, $a: 0.1, $d: 40px) {
@ -502,8 +492,8 @@
} }
@mixin cClickIconButtonLayout() { @mixin cClickIconButtonLayout() {
$pLR: 4px; $pLR: 5px;
$pTB: 4px; $pTB: 5px;
padding: $pTB $pLR; padding: $pTB $pLR;
&:before, &:before,
@ -522,6 +512,7 @@
@include cControl(); @include cControl();
@include cClickIconButtonLayout(); @include cClickIconButtonLayout();
background: none; background: none;
color: $colorClickIconButton;
box-shadow: none; box-shadow: none;
cursor: pointer; cursor: pointer;
transition: $transOut; transition: $transOut;
@ -530,7 +521,8 @@
@include hover() { @include hover() {
transition: $transIn; transition: $transIn;
background: $colorClickIconButtonBgHov; background: $colorClickIconButtonBgHov;
color: $colorClickIconButtonFgHov; //color: $colorClickIconButtonFgHov;
filter: $filterHov;
} }
&[class*="--major"] { &[class*="--major"] {

View File

@ -198,3 +198,27 @@ tr {
.u-alert { @include uIndicator($colorAlert, $colorAlertFg, $glyph-icon-alert-triangle); } .u-alert { @include uIndicator($colorAlert, $colorAlertFg, $glyph-icon-alert-triangle); }
.u-error { @include uIndicator($colorError, $colorErrorFg, $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-table-control-bar {
.c-icon-button,
.c-click-icon, .c-click-icon,
.c-button { .c-button {
&__label { &__label {

View File

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

View File

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

View File

@ -21,45 +21,70 @@
*****************************************************************************/ *****************************************************************************/
<template> <template>
<div <div
class="c-so-view has-local-controls" class="c-so-view"
:class="{ :class="[
statusClass,
'c-so-view--' + domainObject.type,
{
'c-so-view--no-frame': !hasFrame, 'c-so-view--no-frame': !hasFrame,
'has-complex-content': complexContent, 'has-complex-content': complexContent
'is-missing': domainObject.status === 'missing' }
}" ]"
> >
<div class="c-so-view__header"> <div
class="c-so-view__header"
>
<div class="c-object-label" <div class="c-object-label"
:class="{ :class="[ statusClass ]"
classList,
'is-missing': domainObject.status === 'missing'
}"
> >
<div class="c-object-label__type-icon" <div class="c-object-label__type-icon"
:class="cssClass" :class="cssClass"
> >
<span class="is-missing__indicator" <span class="is-status__indicator"
title="This item is missing" :title="`This item is ${status}`"
></span> ></span>
</div> </div>
<div class="c-object-label__name"> <div class="c-object-label__name">
{{ domainObject && domainObject.name }} {{ domainObject && domainObject.name }}
</div> </div>
</div> </div>
<context-menu-drop-down
:object-path="objectPath" <div
/> class="c-so-view__frame-controls"
</div> :class="{
<div class="c-so-view__local-controls c-so-view__view-large h-local-controls c-local-controls--show-on-hover"> 'c-so-view__frame-controls--no-frame': !hasFrame,
'has-complex-content': complexContent
}"
>
<div class="c-so-view__frame-controls__btns">
<button <button
class="c-button icon-expand" 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" title="View Large"
@click="expand" @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> ></button>
</div> </div>
<div class="is-missing__indicator" </div>
title="This item is missing"
></div>
<object-view <object-view
ref="objectView" ref="objectView"
class="c-so-view__object-view" class="c-so-view__object-view"
@ -68,13 +93,13 @@
:object-path="objectPath" :object-path="objectPath"
:layout-font-size="layoutFontSize" :layout-font-size="layoutFontSize"
:layout-font="layoutFont" :layout-font="layoutFont"
@change-action-collection="setActionCollection"
/> />
</div> </div>
</template> </template>
<script> <script>
import ObjectView from './ObjectView.vue'; import ObjectView from './ObjectView.vue';
import ContextMenuDropDown from './contextMenuDropDown.vue';
import PreviewHeader from '@/ui/preview/preview-header.vue'; import PreviewHeader from '@/ui/preview/preview-header.vue';
import Vue from 'vue'; import Vue from 'vue';
@ -89,8 +114,7 @@ const SIMPLE_CONTENT_TYPES = [
export default { export default {
inject: ['openmct'], inject: ['openmct'],
components: { components: {
ObjectView, ObjectView
ContextMenuDropDown
}, },
props: { props: {
domainObject: { domainObject: {
@ -117,22 +141,32 @@ export default {
}, },
data() { data() {
let objectType = this.openmct.types.get(this.domainObject.type); let objectType = this.openmct.types.get(this.domainObject.type);
let cssClass = objectType && objectType.definition ? objectType.definition.cssClass : 'icon-object-unknown'; let cssClass = objectType && objectType.definition ? objectType.definition.cssClass : 'icon-object-unknown';
let complexContent = !SIMPLE_CONTENT_TYPES.includes(this.domainObject.type); let complexContent = !SIMPLE_CONTENT_TYPES.includes(this.domainObject.type);
return { return {
cssClass, cssClass,
complexContent complexContent,
statusBarItems: [],
status: ''
}; };
}, },
computed: { computed: {
classList() { statusClass() {
const classList = this.domainObject.classList; return (this.status) ? `is-status--${this.status}` : '';
if (!classList || !classList.length) {
return '';
} }
},
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: { methods: {
@ -162,6 +196,7 @@ export default {
}, },
getPreviewHeader() { getPreviewHeader() {
const domainObject = this.objectPath[0]; const domainObject = this.objectPath[0];
const actionCollection = this.actionCollection;
const preview = new Vue({ const preview = new Vue({
components: { components: {
PreviewHeader PreviewHeader
@ -172,16 +207,41 @@ export default {
}, },
data() { data() {
return { 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; return preview.$mount().$el;
}, },
getSelectionContext() { getSelectionContext() {
return this.$refs.objectView.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> <template>
<a <a
class="c-tree__item__label c-object-label" class="c-tree__item__label c-object-label"
:class="{ :class="[statusClass]"
classList,
'is-missing': observedObject.status === 'missing'
}"
draggable="true" draggable="true"
:href="objectLink" :href="objectLink"
@dragstart="dragStart" @dragstart="dragStart"
@ -14,13 +11,11 @@
class="c-tree__item__type-icon c-object-label__type-icon" class="c-tree__item__type-icon c-object-label__type-icon"
:class="typeClass" :class="typeClass"
> >
<span class="is-missing__indicator" <span class="is-status__indicator"
title="This item is missing" :title="`This item is ${status}`"
></span> ></span>
</div> </div>
<div class="c-tree__item__name c-object-label__name" <div class="c-tree__item__name c-object-label__name">
:class="classList"
>
{{ observedObject.name }} {{ observedObject.name }}
</div> </div>
</a> </a>
@ -51,18 +46,11 @@ export default {
}, },
data() { data() {
return { return {
observedObject: this.domainObject observedObject: this.domainObject,
status: ''
}; };
}, },
computed: { computed: {
classList() {
const classList = this.observedObject.classList;
if (!classList || !classList.length) {
return '';
}
return classList.join(' ');
},
typeClass() { typeClass() {
let type = this.openmct.types.get(this.observedObject.type); let type = this.openmct.types.get(this.observedObject.type);
if (!type) { if (!type) {
@ -70,6 +58,9 @@ export default {
} }
return type.definition.cssClass; return type.definition.cssClass;
},
statusClass() {
return (this.status) ? `is-status--${this.status}` : '';
} }
}, },
mounted() { mounted() {
@ -80,8 +71,13 @@ export default {
this.$once('hook:destroyed', removeListener); 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); this.previewAction = new PreviewAction(this.openmct);
}, },
destroyed() {
this.removeStatusListener();
},
methods: { methods: {
navigateOrPreview(event) { navigateOrPreview(event) {
if (this.openmct.editor.isEditing()) { if (this.openmct.editor.isEditing()) {
@ -112,6 +108,9 @@ export default {
// (eg. notabook.) // (eg. notabook.)
event.dataTransfer.setData("openmct/domain-object-path", serializedPath); event.dataTransfer.setData("openmct/domain-object-path", serializedPath);
event.dataTransfer.setData(`openmct/domain-object/${keyString}`, this.domainObject); 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(); this.styleRuleManager.destroy();
delete this.styleRuleManager; delete this.styleRuleManager;
} }
if (this.actionCollection) {
this.actionCollection.destroy();
delete this.actionCollection;
}
}, },
created() { created() {
this.debounceUpdateView = _.debounce(this.updateView, 10); this.debounceUpdateView = _.debounce(this.updateView, 10);
@ -149,6 +154,7 @@ export default {
let keys = Object.keys(styleObj); let keys = Object.keys(styleObj);
let elemToStyle = this.getStyleReceiver(); let elemToStyle = this.getStyleReceiver();
keys.forEach(key => { keys.forEach(key => {
if (elemToStyle) { if (elemToStyle) {
if ((typeof styleObj[key] === 'string') && (styleObj[key].indexOf('__no_value') > -1)) { 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()); this.currentView.show(this.viewContainer, this.openmct.editor.isEditing());
if (immediatelySelect) { if (immediatelySelect) {
@ -216,6 +223,14 @@ export default {
this.openmct.objectViews.on('clearData', this.clearData); 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) { show(object, viewKey, immediatelySelect, currentObjectPath) {
this.updateStyle(); this.updateStyle();

View File

@ -2,20 +2,21 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
&.is-missing {
border: $borderMissing;
}
/*************************** HEADER */ /*************************** HEADER */
&__header { &__header {
flex: 0 0 auto; flex: 0 0 auto;
display: flex; display: flex;
font-size: 1.05em; justify-content: space-between;
align-items: center; align-items: center;
margin-bottom: $interiorMarginSm; margin-bottom: $interiorMarginSm;
padding: 1px 2px; padding: 3px;
.c-object-label { .c-object-label {
font-size: 1.05em;
&__type-icon {
opacity: $objectLabelTypeIconOpacity;
}
&__name { &__name {
filter: $objectLabelNameFilter; filter: $objectLabelNameFilter;
} }
@ -31,45 +32,118 @@
} }
} }
/*************************** FRAME CONTROLS */
&__frame-controls {
display: flex;
&__btns,
&__more {
flex: 0 0 auto;
}
.is-in-small-container &,
.c-fl-frame & {
[class*="__label"] {
// button labels
display: none;
}
}
}
/*************************** HIDDEN FRAME */
&--no-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 { > .c-so-view__header {
display: none; display: none;
} }
&.is-missing {
@include isMissing($absPos: true);
.is-missing__indicator {
top: $interiorMargin;
left: $interiorMargin;
} }
/* HOVERS */
&:hover {
> .c-so-view__header {
visibility: visible;
} }
} }
&__local-controls { &[class*='is-status'] {
// View Large button border: $borderMissing;
box-shadow: $colorLocalControlOvrBg 0 0 0 2px; }
position: absolute; }
top: $interiorMargin; right: $interiorMargin;
z-index: 10; /*************************** 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-click-icon,
.c-button { .c-button,
.c-icon-button {
// Shrink buttons a bit when they appear in a frame // Shrink buttons a bit when they appear in a frame
border-radius: $smallCr !important; border-radius: $smallCr !important;
font-size: 0.9em; font-size: 0.9em;
padding: 3px 5px; padding: 5px;
}
&__view-large {
display: none;
} }
&.has-complex-content { &.has-complex-content {
> .c-so-view__view-large { display: block; } > .c-so-view__view-large { display: block; }
} }
&.is-status--missing {
border: $borderMissing;
}
&__object-view { &__object-view {
display: flex; display: flex;
flex: 1 1 auto; flex: 1 1 auto;
@ -86,4 +160,5 @@
// This element is the recipient for object styling; cannot be display: contents // This element is the recipient for object styling; cannot be display: contents
flex: 1 1 auto; flex: 1 1 auto;
overflow: hidden; overflow: hidden;
display: contents;
} }

View File

@ -22,18 +22,20 @@
opacity: $objectLabelTypeIconOpacity; opacity: $objectLabelTypeIconOpacity;
} }
&.is-missing { .is-status__indicator {
@include isMissing($absPos: true); position: absolute;
[class*='__type-icon']:before,
[class*='__type-icon']:after{
opacity: $opacityMissing;
}
.is-missing__indicator {
right: -3px; right: -3px;
top: -3px; top: -3px;
transform: scale(0.7); transform: scale(0.5);
}
&[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 class="c-inspector__header">
<div v-if="!multiSelect" <div v-if="!multiSelect"
class="c-inspector__selected c-object-label" class="c-inspector__selected c-object-label"
:class="{'is-missing': isMissing }" :class="[statusClass]"
> >
<div class="c-object-label__type-icon" <div class="c-object-label__type-icon"
:class="typeCssClass" :class="typeCssClass"
> >
<span class="is-missing__indicator" <span class="is-status__indicator"
title="This item is missing" :title="`This item is ${status}`"
></span> ></span>
</div> </div>
<span v-if="!singleSelectNonObject" <span v-if="!singleSelectNonObject"
@ -37,8 +37,10 @@ export default {
data() { data() {
return { return {
domainObject: {}, domainObject: {},
keyString: undefined,
multiSelect: false, multiSelect: false,
itemsSelected: 0 itemsSelected: 0,
status: undefined
}; };
}, },
computed: { computed: {
@ -58,9 +60,8 @@ export default {
singleSelectNonObject() { singleSelectNonObject() {
return !this.item.identifier && !this.multiSelect; return !this.item.identifier && !this.multiSelect;
}, },
isMissing() { statusClass() {
// safe check this.domainObject since for layout objects this.domainOjbect is undefined return this.status ? `is-status--${this.status}` : '';
return this.domainObject && this.domainObject.status === 'missing';
} }
}, },
mounted() { mounted() {
@ -69,25 +70,45 @@ export default {
}, },
beforeDestroy() { beforeDestroy() {
this.openmct.selection.off('change', this.updateSelection); this.openmct.selection.off('change', this.updateSelection);
if (this.statusUnsubscribe) {
this.statusUnsubscribe();
}
}, },
methods: { methods: {
updateSelection(selection) { updateSelection(selection) {
if (this.statusUnsubscribe) {
this.statusUnsubscribe();
this.statusUnsubscribe = undefined;
}
if (selection.length === 0 || selection[0].length === 0) { if (selection.length === 0 || selection[0].length === 0) {
this.domainObject = {}; this.resetDomainObject();
return; return;
} }
if (selection.length > 1) { if (selection.length > 1) {
this.multiSelect = true; this.multiSelect = true;
this.domainObject = {};
this.itemsSelected = selection.length; this.itemsSelected = selection.length;
this.resetDomainObject();
return; return;
} else { } else {
this.multiSelect = false; this.multiSelect = false;
this.domainObject = selection[0][0].context.item; 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; filter: $objectLabelNameFilter;
} }
.c-object-label__type-icon {
opacity: $objectLabelTypeIconOpacity;
}
&--non-domain-object .c-object-label__name { &--non-domain-object .c-object-label__name {
font-style: italic; font-style: italic;
} }

View File

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

View File

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

View File

@ -4,36 +4,21 @@
class="l-browse-bar__view-switcher c-ctrl-wrapper c-ctrl-wrapper--menus-left" class="l-browse-bar__view-switcher c-ctrl-wrapper c-ctrl-wrapper--menus-left"
> >
<button <button
class="c-button--menu" class="c-icon-button c-button--menu"
:class="currentView.cssClass" :class="currentView.cssClass"
title="Change the current view" 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 }} {{ currentView.name }}
</span> </span>
</button> </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> </div>
</template> </template>
<script> <script>
export default { export default {
inject: ['openmct'],
props: { props: {
currentView: { currentView: {
type: Object, type: Object,
@ -44,26 +29,16 @@ export default {
required: true required: true
} }
}, },
data() {
return {
showViewMenu: false
};
},
mounted() {
document.addEventListener('click', this.hideViewMenu);
},
destroyed() {
document.removeEventListener('click', this.hideViewMenu);
},
methods: { methods: {
setView(view) { setView(view) {
this.$emit('setView', view); this.$emit('setView', view);
}, },
toggleViewMenu() { showMenu() {
this.showViewMenu = !this.showViewMenu; const elementBoundingClientRect = this.$el.getBoundingClientRect();
}, const x = elementBoundingClientRect.x;
hideViewMenu() { const y = elementBoundingClientRect.y + elementBoundingClientRect.height;
this.showViewMenu = false;
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; filter: $objectLabelNameFilter;
} }
.c-object-label__type-icon {
opacity: $objectLabelTypeIconOpacity;
}
&__object-name--w { &__object-name--w {
@include headerFont(1.5em); @include headerFont(1.5em);
margin-left: $interiorMarginLg; margin-left: $interiorMarginLg;
min-width: 0; min-width: 0;
.is-missing__indicator { .is-status__indicator {
right: -5px !important; right: -5px !important;
top: -4px !important; top: -4px !important;
} }

View File

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

View File

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

View File

@ -32,4 +32,14 @@ export default class ViewHistoricalDataAction extends PreviewAction {
this.cssClass = 'icon-eye-open'; this.cssClass = 'icon-eye-open';
this.hideInDefaultMenu = true; 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 () { export default function () {
return function (openmct) { return function (openmct) {
openmct.contextMenu.registerAction(new PreviewAction(openmct)); openmct.actions.register(new PreviewAction(openmct));
openmct.contextMenu.registerAction(new ViewHistoricalDataAction(openmct)); openmct.actions.register(new ViewHistoricalDataAction(openmct));
}; };
} }

View File

@ -13,13 +13,25 @@
</div> </div>
</div> </div>
<div class="l-browse-bar__end"> <div class="l-browse-bar__end">
<div class="l-browse-bar__actions">
<view-switcher <view-switcher
:v-if="!hideViewSwitcher" :v-if="!hideViewSwitcher"
:views="views" :views="views"
:current-view="currentView" :current-view="currentView"
@setView="setView"
/> />
<div class="l-browse-bar__actions">
<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> </div>
</div> </div>
@ -27,6 +39,11 @@
<script> <script>
import ViewSwitcher from '../../ui/layout/ViewSwitcher.vue'; import ViewSwitcher from '../../ui/layout/ViewSwitcher.vue';
const HIDDEN_ACTIONS = [
'remove',
'move',
'preview'
];
export default { export default {
inject: [ inject: [
@ -59,16 +76,53 @@ export default {
default: () => { default: () => {
return []; return [];
} }
},
actionCollection: {
type: Object,
default: () => {
return undefined;
}
} }
}, },
data() { data() {
return { 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: { methods: {
setView(view) { setView(view) {
this.$emit('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();