mirror of
https://github.com/nasa/openmct.git
synced 2025-04-09 04:14:32 +00:00
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:
parent
d232dacc65
commit
6375ecda34
@ -143,8 +143,8 @@ define([
|
||||
"$window"
|
||||
],
|
||||
"group": "windowing",
|
||||
"cssClass": "icon-new-window",
|
||||
"priority": "preferred"
|
||||
"priority": 10,
|
||||
"cssClass": "icon-new-window"
|
||||
}
|
||||
],
|
||||
"runs": [
|
||||
|
@ -139,7 +139,9 @@ define([
|
||||
],
|
||||
"description": "Edit",
|
||||
"category": "view-control",
|
||||
"cssClass": "major icon-pencil"
|
||||
"cssClass": "major icon-pencil",
|
||||
"group": "action",
|
||||
"priority": 10
|
||||
},
|
||||
{
|
||||
"key": "properties",
|
||||
@ -150,6 +152,8 @@ define([
|
||||
"implementation": PropertiesAction,
|
||||
"cssClass": "major icon-pencil",
|
||||
"name": "Edit Properties...",
|
||||
"group": "action",
|
||||
"priority": 10,
|
||||
"description": "Edit properties of this object.",
|
||||
"depends": [
|
||||
"dialogService"
|
||||
|
@ -20,12 +20,12 @@
|
||||
at runtime from the About dialog for additional information.
|
||||
-->
|
||||
<div class="c-object-label"
|
||||
ng-class="{ 'is-missing': model.status === 'missing' }"
|
||||
ng-class="{ 'is-status--missing': model.status === 'missing' }"
|
||||
>
|
||||
<div class="c-object-label__type-icon {{type.getCssClass()}}"
|
||||
ng-class="{ 'l-icon-link':location.isLink() }"
|
||||
>
|
||||
<span class="is-missing__indicator" title="This item is missing"></span>
|
||||
<span class="is-status__indicator" title="This item is missing or suspect"></span>
|
||||
</div>
|
||||
<div class='c-object-label__name'>{{model.name}}</div>
|
||||
</div>
|
||||
|
@ -66,6 +66,8 @@ define([
|
||||
"description": "Move object to another location.",
|
||||
"cssClass": "icon-move",
|
||||
"category": "contextual",
|
||||
"group": "action",
|
||||
"priority": 9,
|
||||
"implementation": MoveAction,
|
||||
"depends": [
|
||||
"policyService",
|
||||
@ -79,6 +81,8 @@ define([
|
||||
"description": "Duplicate object to another location.",
|
||||
"cssClass": "icon-duplicate",
|
||||
"category": "contextual",
|
||||
"group": "action",
|
||||
"priority": 8,
|
||||
"implementation": CopyAction,
|
||||
"depends": [
|
||||
"$log",
|
||||
@ -95,6 +99,8 @@ define([
|
||||
"description": "Create Link to object in another location.",
|
||||
"cssClass": "icon-link",
|
||||
"category": "contextual",
|
||||
"group": "action",
|
||||
"priority": 7,
|
||||
"implementation": LinkAction,
|
||||
"depends": [
|
||||
"policyService",
|
||||
|
@ -47,6 +47,8 @@ define([
|
||||
"implementation": ExportAsJSONAction,
|
||||
"category": "contextual",
|
||||
"cssClass": "icon-export",
|
||||
"group": "json",
|
||||
"priority": 2,
|
||||
"depends": [
|
||||
"openmct",
|
||||
"exportService",
|
||||
@ -61,6 +63,8 @@ define([
|
||||
"implementation": ImportAsJSONAction,
|
||||
"category": "contextual",
|
||||
"cssClass": "icon-import",
|
||||
"group": "json",
|
||||
"priority": 2,
|
||||
"depends": [
|
||||
"exportService",
|
||||
"identifierService",
|
||||
|
@ -242,7 +242,11 @@ define([
|
||||
|
||||
this.overlays = new OverlayAPI.default();
|
||||
|
||||
this.contextMenu = new api.ContextMenuRegistry();
|
||||
this.menus = new api.MenuAPI(this);
|
||||
|
||||
this.actions = new api.ActionsAPI(this);
|
||||
|
||||
this.status = new api.StatusAPI(this);
|
||||
|
||||
this.router = new ApplicationRouter();
|
||||
|
||||
@ -271,6 +275,7 @@ define([
|
||||
this.install(this.plugins.URLTimeSettingsSynchronizer());
|
||||
this.install(this.plugins.NotificationIndicator());
|
||||
this.install(this.plugins.NewFolderAction());
|
||||
this.install(this.plugins.ViewDatumAction());
|
||||
}
|
||||
|
||||
MCT.prototype = Object.create(EventEmitter.prototype);
|
||||
|
@ -35,5 +35,5 @@ export default function LegacyActionAdapter(openmct, legacyActions) {
|
||||
|
||||
legacyActions.filter(contextualCategoryOnly)
|
||||
.map(LegacyAction => new LegacyContextMenuAction(openmct, LegacyAction))
|
||||
.forEach(openmct.contextMenu.registerAction);
|
||||
.forEach(openmct.actions.register);
|
||||
}
|
||||
|
@ -31,6 +31,8 @@ export default class LegacyContextMenuAction {
|
||||
this.description = LegacyAction.definition.description;
|
||||
this.cssClass = LegacyAction.definition.cssClass;
|
||||
this.LegacyAction = LegacyAction;
|
||||
this.group = LegacyAction.definition.group;
|
||||
this.priority = LegacyAction.definition.priority;
|
||||
}
|
||||
|
||||
invoke(objectPath) {
|
||||
|
189
src/api/actions/ActionCollection.js
Normal file
189
src/api/actions/ActionCollection.js
Normal 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;
|
144
src/api/actions/ActionsAPI.js
Normal file
144
src/api/actions/ActionsAPI.js
Normal 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;
|
113
src/api/actions/ActionsAPISpec.js
Normal file
113
src/api/actions/ActionsAPISpec.js
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
@ -28,9 +28,10 @@ define([
|
||||
'./telemetry/TelemetryAPI',
|
||||
'./indicators/IndicatorAPI',
|
||||
'./notifications/NotificationAPI',
|
||||
'./contextMenu/ContextMenuAPI',
|
||||
'./Editor'
|
||||
|
||||
'./Editor',
|
||||
'./menu/MenuAPI',
|
||||
'./actions/ActionsAPI',
|
||||
'./status/StatusAPI'
|
||||
], function (
|
||||
TimeAPI,
|
||||
ObjectAPI,
|
||||
@ -39,8 +40,10 @@ define([
|
||||
TelemetryAPI,
|
||||
IndicatorAPI,
|
||||
NotificationAPI,
|
||||
ContextMenuAPI,
|
||||
EditorAPI
|
||||
EditorAPI,
|
||||
MenuAPI,
|
||||
ActionsAPI,
|
||||
StatusAPI
|
||||
) {
|
||||
return {
|
||||
TimeAPI: TimeAPI,
|
||||
@ -51,6 +54,8 @@ define([
|
||||
IndicatorAPI: IndicatorAPI,
|
||||
NotificationAPI: NotificationAPI.default,
|
||||
EditorAPI: EditorAPI,
|
||||
ContextMenuRegistry: ContextMenuAPI.default
|
||||
MenuAPI: MenuAPI.default,
|
||||
ActionsAPI: ActionsAPI.default,
|
||||
StatusAPI: StatusAPI.default
|
||||
};
|
||||
});
|
||||
|
@ -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>
|
@ -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
67
src/api/menu/MenuAPI.js
Normal 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
125
src/api/menu/MenuAPISpec.js
Normal 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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
52
src/api/menu/components/Menu.vue
Normal file
52
src/api/menu/components/Menu.vue
Normal 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
94
src/api/menu/menu.js
Normal 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;
|
@ -22,6 +22,7 @@ class OverlayAPI {
|
||||
this.dismissLastOverlay();
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
@ -127,6 +128,7 @@ class OverlayAPI {
|
||||
|
||||
return progressDialog;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default OverlayAPI;
|
||||
|
67
src/api/status/StatusAPI.js
Normal file
67
src/api/status/StatusAPI.js
Normal 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);
|
||||
};
|
||||
}
|
||||
}
|
85
src/api/status/StatusAPISpec.js
Normal file
85
src/api/status/StatusAPISpec.js
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
@ -44,6 +44,7 @@
|
||||
<script>
|
||||
|
||||
const CONTEXT_MENU_ACTIONS = [
|
||||
'viewDatumAction',
|
||||
'viewHistoricalData',
|
||||
'remove'
|
||||
];
|
||||
@ -129,6 +130,7 @@ export default {
|
||||
let limit;
|
||||
|
||||
if (this.shouldUpdate(newTimestamp)) {
|
||||
this.datum = datum;
|
||||
this.timestamp = newTimestamp;
|
||||
this.value = this.formats[this.valueKey].format(datum);
|
||||
limit = this.limitEvaluator.evaluate(datum, this.valueMetadata);
|
||||
@ -175,8 +177,25 @@ export default {
|
||||
this.resetValues();
|
||||
this.timestampKey = timeSystem.key;
|
||||
},
|
||||
getView() {
|
||||
return {
|
||||
getViewContext: () => {
|
||||
return {
|
||||
viewHistoricalData: true,
|
||||
viewDatumAction: true,
|
||||
getDatum: () => {
|
||||
return this.datum;
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
},
|
||||
showContextMenu(event) {
|
||||
this.openmct.contextMenu._showContextMenuForObjectPath(this.currentObjectPath, event.x, event.y, CONTEXT_MENU_ACTIONS);
|
||||
let actionCollection = this.openmct.actions.get(this.currentObjectPath, this.getView());
|
||||
let allActions = actionCollection.getActionsObject();
|
||||
let applicableActions = CONTEXT_MENU_ACTIONS.map(key => allActions[key]);
|
||||
|
||||
this.openmct.menus.showMenu(event.x, event.y, applicableActions);
|
||||
},
|
||||
resetValues() {
|
||||
this.value = '---';
|
||||
|
@ -23,6 +23,7 @@
|
||||
export default class ClearDataAction {
|
||||
constructor(openmct, appliesToObjects) {
|
||||
this.name = 'Clear Data for Object';
|
||||
this.key = 'clear-data-action';
|
||||
this.description = 'Clears current data for object, unsubscribes and resubscribes to data';
|
||||
this.cssClass = 'icon-clear-data';
|
||||
|
||||
|
@ -53,7 +53,7 @@ define([
|
||||
openmct.indicators.add(indicator);
|
||||
}
|
||||
|
||||
openmct.contextMenu.registerAction(new ClearDataAction.default(openmct, appliesToObjects));
|
||||
openmct.actions.register(new ClearDataAction.default(openmct, appliesToObjects));
|
||||
};
|
||||
};
|
||||
});
|
||||
|
@ -26,12 +26,12 @@ import ClearDataAction from '../clearDataAction.js';
|
||||
describe('When the Clear Data Plugin is installed,', function () {
|
||||
const mockObjectViews = jasmine.createSpyObj('objectViews', ['emit']);
|
||||
const mockIndicatorProvider = jasmine.createSpyObj('indicators', ['add']);
|
||||
const mockContextMenuProvider = jasmine.createSpyObj('contextMenu', ['registerAction']);
|
||||
const mockActionsProvider = jasmine.createSpyObj('actions', ['register']);
|
||||
|
||||
const openmct = {
|
||||
objectViews: mockObjectViews,
|
||||
indicators: mockIndicatorProvider,
|
||||
contextMenu: mockContextMenuProvider,
|
||||
actions: mockActionsProvider,
|
||||
install: function (plugin) {
|
||||
plugin(this);
|
||||
}
|
||||
@ -51,7 +51,7 @@ describe('When the Clear Data Plugin is installed,', function () {
|
||||
it('Clear Data context menu action is installed', function () {
|
||||
openmct.install(ClearDataActionPlugin([]));
|
||||
|
||||
expect(mockContextMenuProvider.registerAction).toHaveBeenCalled();
|
||||
expect(mockActionsProvider.register).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('clear data action emits a clearData event when invoked', function () {
|
||||
|
@ -64,9 +64,16 @@ define([
|
||||
components: {
|
||||
AlphanumericFormatView: AlphanumericFormatView.default
|
||||
},
|
||||
template: '<alphanumeric-format-view></alphanumeric-format-view>'
|
||||
template: '<alphanumeric-format-view ref="alphanumericFormatView"></alphanumeric-format-view>'
|
||||
});
|
||||
},
|
||||
getViewContext() {
|
||||
if (component) {
|
||||
return component.$refs.alphanumericFormatView.getViewContext();
|
||||
} else {
|
||||
return {};
|
||||
}
|
||||
},
|
||||
destroy: function () {
|
||||
component.$destroy();
|
||||
component = undefined;
|
||||
|
34
src/plugins/displayLayout/actions/CopyToClipboardAction.js
Normal file
34
src/plugins/displayLayout/actions/CopyToClipboardAction.js
Normal 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';
|
||||
}
|
||||
}
|
@ -142,14 +142,18 @@ export default {
|
||||
this.domainObject = domainObject;
|
||||
this.currentObjectPath = [this.domainObject].concat(this.objectPath.slice());
|
||||
this.$nextTick(() => {
|
||||
let childContext = this.$refs.objectFrame.getSelectionContext();
|
||||
childContext.item = domainObject;
|
||||
childContext.layoutItem = this.item;
|
||||
childContext.index = this.index;
|
||||
this.context = childContext;
|
||||
this.removeSelectable = this.openmct.selection.selectable(
|
||||
this.$el, this.context, this.immediatelySelect || this.initSelect);
|
||||
delete this.immediatelySelect;
|
||||
let reference = this.$refs.objectFrame;
|
||||
|
||||
if (reference) {
|
||||
let childContext = this.$refs.objectFrame.getSelectionContext();
|
||||
childContext.item = domainObject;
|
||||
childContext.layoutItem = this.item;
|
||||
childContext.index = this.index;
|
||||
this.context = childContext;
|
||||
this.removeSelectable = this.openmct.selection.selectable(
|
||||
this.$el, this.context, this.immediatelySelect || this.initSelect);
|
||||
delete this.immediatelySelect;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -30,18 +30,15 @@
|
||||
>
|
||||
<div
|
||||
v-if="domainObject"
|
||||
class="u-style-receiver c-telemetry-view"
|
||||
:class="{
|
||||
styleClass,
|
||||
'is-missing': domainObject.status === 'missing'
|
||||
}"
|
||||
class="c-telemetry-view"
|
||||
:class="[statusClass]"
|
||||
:style="styleObject"
|
||||
:data-font-size="item.fontSize"
|
||||
:data-font="item.font"
|
||||
@contextmenu.prevent="showContextMenu"
|
||||
>
|
||||
<div class="is-missing__indicator"
|
||||
title="This item is missing"
|
||||
<div class="is-status__indicator"
|
||||
:title="`This item is ${status}`"
|
||||
></div>
|
||||
<div
|
||||
v-if="showLabel"
|
||||
@ -76,10 +73,11 @@
|
||||
import LayoutFrame from './LayoutFrame.vue';
|
||||
import printj from 'printj';
|
||||
import conditionalStylesMixin from "../mixins/objectStyles-mixin";
|
||||
import { getDefaultNotebook } from '@/plugins/notebook/utils/notebook-storage.js';
|
||||
|
||||
const DEFAULT_TELEMETRY_DIMENSIONS = [10, 5];
|
||||
const DEFAULT_POSITION = [1, 1];
|
||||
const CONTEXT_MENU_ACTIONS = ['viewHistoricalData'];
|
||||
const CONTEXT_MENU_ACTIONS = ['copyToClipboard', 'copyToNotebook', 'viewHistoricalData'];
|
||||
|
||||
export default {
|
||||
makeDefinition(openmct, gridSize, domainObject, position) {
|
||||
@ -129,13 +127,18 @@ export default {
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
currentObjectPath: undefined,
|
||||
datum: undefined,
|
||||
formats: undefined,
|
||||
domainObject: undefined,
|
||||
currentObjectPath: undefined
|
||||
formats: undefined,
|
||||
viewKey: `alphanumeric-format-${Math.random()}`,
|
||||
status: ''
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
statusClass() {
|
||||
return (this.status) ? `is-status--${this.status}` : '';
|
||||
},
|
||||
showLabel() {
|
||||
let displayMode = this.item.displayMode;
|
||||
|
||||
@ -213,9 +216,13 @@ export default {
|
||||
this.openmct.objects.get(this.item.identifier)
|
||||
.then(this.setObject);
|
||||
this.openmct.time.on("bounds", this.refreshData);
|
||||
|
||||
this.status = this.openmct.status.get(this.item.identifier);
|
||||
this.removeStatusListener = this.openmct.status.observe(this.item.identifier, this.setStatus);
|
||||
},
|
||||
destroyed() {
|
||||
this.removeSubscription();
|
||||
this.removeStatusListener();
|
||||
|
||||
if (this.removeSelectable) {
|
||||
this.removeSelectable();
|
||||
@ -224,6 +231,12 @@ export default {
|
||||
this.openmct.time.off("bounds", this.refreshData);
|
||||
},
|
||||
methods: {
|
||||
formattedValueForCopy() {
|
||||
const timeFormatterKey = this.openmct.time.timeSystem().key;
|
||||
const timeFormatter = this.formats[timeFormatterKey];
|
||||
|
||||
return `At ${timeFormatter.format(this.datum)} ${this.domainObject.name} had a value of ${this.telemetryValue} ${this.unit}`;
|
||||
},
|
||||
requestHistoricalData() {
|
||||
let bounds = this.openmct.time.bounds();
|
||||
let options = {
|
||||
@ -261,6 +274,16 @@ export default {
|
||||
this.requestHistoricalData(this.domainObject);
|
||||
}
|
||||
},
|
||||
getView() {
|
||||
return {
|
||||
getViewContext: () => {
|
||||
return {
|
||||
viewHistoricalData: true,
|
||||
formattedValueForCopy: this.formattedValueForCopy
|
||||
};
|
||||
}
|
||||
};
|
||||
},
|
||||
setObject(domainObject) {
|
||||
this.domainObject = domainObject;
|
||||
this.keyString = this.openmct.objects.makeKeyString(domainObject.identifier);
|
||||
@ -288,8 +311,33 @@ export default {
|
||||
updateTelemetryFormat(format) {
|
||||
this.$emit('formatChanged', this.item, format);
|
||||
},
|
||||
showContextMenu(event) {
|
||||
this.openmct.contextMenu._showContextMenuForObjectPath(this.currentObjectPath, event.x, event.y, CONTEXT_MENU_ACTIONS);
|
||||
async getContextMenuActions() {
|
||||
const defaultNotebook = getDefaultNotebook();
|
||||
const domainObject = defaultNotebook && await this.openmct.objects.get(defaultNotebook.notebookMeta.identifier);
|
||||
const actionCollection = this.openmct.actions.get(this.currentObjectPath, this.getView());
|
||||
const actionsObject = actionCollection.getActionsObject();
|
||||
|
||||
let copyToNotebookAction = actionsObject.copyToNotebook;
|
||||
|
||||
if (defaultNotebook) {
|
||||
const defaultPath = domainObject && `${domainObject.name} - ${defaultNotebook.section.name} - ${defaultNotebook.page.name}`;
|
||||
copyToNotebookAction.name = `Copy to Notebook ${defaultPath}`;
|
||||
} else {
|
||||
actionsObject.copyToNotebook = undefined;
|
||||
delete actionsObject.copyToNotebook;
|
||||
}
|
||||
|
||||
return CONTEXT_MENU_ACTIONS.map(actionKey => {
|
||||
return actionsObject[actionKey];
|
||||
}).filter(action => action !== undefined);
|
||||
},
|
||||
async showContextMenu(event) {
|
||||
const contextMenuActions = await this.getContextMenuActions();
|
||||
|
||||
this.openmct.menus.showMenu(event.x, event.y, contextMenuActions);
|
||||
},
|
||||
setStatus(status) {
|
||||
this.status = status;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -7,7 +7,6 @@
|
||||
flex: 1 1 auto;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
// justify-content: center;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
padding: $interiorMargin;
|
||||
@ -27,14 +26,13 @@
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
@include isMissing($absPos: true);
|
||||
|
||||
.is-missing__indicator {
|
||||
.is-status__indicator {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
&.is-missing {
|
||||
&[class*='is-status'] {
|
||||
border: $borderMissing;
|
||||
}
|
||||
}
|
||||
|
@ -26,9 +26,12 @@ import objectUtils from 'objectUtils';
|
||||
import DisplayLayoutType from './DisplayLayoutType.js';
|
||||
import DisplayLayoutToolbar from './DisplayLayoutToolbar.js';
|
||||
import AlphaNumericFormatViewProvider from './AlphanumericFormatViewProvider.js';
|
||||
import CopyToClipboardAction from './actions/CopyToClipboardAction';
|
||||
|
||||
export default function DisplayLayoutPlugin(options) {
|
||||
return function (openmct) {
|
||||
openmct.actions.register(new CopyToClipboardAction(openmct));
|
||||
|
||||
openmct.objectViews.addProvider({
|
||||
key: 'layout.view',
|
||||
canView: function (domainObject) {
|
||||
|
@ -1,11 +1,10 @@
|
||||
<template>
|
||||
<a
|
||||
class="l-grid-view__item c-grid-item"
|
||||
:class="{
|
||||
:class="[{
|
||||
'is-alias': item.isAlias === true,
|
||||
'is-missing': item.model.status === 'missing',
|
||||
'c-grid-item--unknown': item.type.cssClass === undefined || item.type.cssClass.indexOf('unknown') !== -1
|
||||
}"
|
||||
}, statusClass]"
|
||||
:href="objectLink"
|
||||
>
|
||||
<div
|
||||
@ -27,8 +26,8 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="c-grid-item__controls">
|
||||
<div class="is-missing__indicator"
|
||||
title="This item is missing"
|
||||
<div class="is-status__indicator"
|
||||
:title="`This item is ${status}`"
|
||||
></div>
|
||||
<div
|
||||
class="icon-people"
|
||||
@ -46,9 +45,10 @@
|
||||
<script>
|
||||
import contextMenuGesture from '../../../ui/mixins/context-menu-gesture';
|
||||
import objectLink from '../../../ui/mixins/object-link';
|
||||
import statusListener from './status-listener';
|
||||
|
||||
export default {
|
||||
mixins: [contextMenuGesture, objectLink],
|
||||
mixins: [contextMenuGesture, objectLink, statusListener],
|
||||
props: {
|
||||
item: {
|
||||
type: Object,
|
||||
|
@ -8,18 +8,18 @@
|
||||
<a
|
||||
ref="objectLink"
|
||||
class="c-object-label"
|
||||
:class="{ 'is-missing': item.model.status === 'missing' }"
|
||||
:class="[statusClass]"
|
||||
:href="objectLink"
|
||||
>
|
||||
<div
|
||||
class="c-object-label__type-icon c-list-item__type-icon"
|
||||
class="c-object-label__type-icon c-list-item__name__type-icon"
|
||||
:class="item.type.cssClass"
|
||||
>
|
||||
<span class="is-missing__indicator"
|
||||
title="This item is missing"
|
||||
<span class="is-status__indicator"
|
||||
:title="`This item is ${status}`"
|
||||
></span>
|
||||
</div>
|
||||
<div class="c-object-label__name c-list-item__name">{{ item.model.name }}</div>
|
||||
<div class="c-object-label__name c-list-item__name__name">{{ item.model.name }}</div>
|
||||
</a>
|
||||
</td>
|
||||
<td class="c-list-item__type">
|
||||
@ -39,9 +39,10 @@
|
||||
import moment from 'moment';
|
||||
import contextMenuGesture from '../../../ui/mixins/context-menu-gesture';
|
||||
import objectLink from '../../../ui/mixins/object-link';
|
||||
import statusListener from './status-listener';
|
||||
|
||||
export default {
|
||||
mixins: [contextMenuGesture, objectLink],
|
||||
mixins: [contextMenuGesture, objectLink, statusListener],
|
||||
props: {
|
||||
item: {
|
||||
type: Object,
|
||||
|
@ -43,9 +43,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
&.is-missing {
|
||||
@include isMissing();
|
||||
|
||||
&[class*='is-status'] {
|
||||
[class*='__type-icon'],
|
||||
[class*='__details'] {
|
||||
opacity: $opacityMissing;
|
||||
|
@ -1,11 +1,19 @@
|
||||
/******************************* LIST ITEM */
|
||||
.c-list-item {
|
||||
&__type-icon {
|
||||
&__name__type-icon {
|
||||
color: $colorItemTreeIcon;
|
||||
}
|
||||
|
||||
&__name {
|
||||
&__name__name {
|
||||
@include ellipsize();
|
||||
|
||||
a & {
|
||||
color: $colorItemFg;
|
||||
}
|
||||
}
|
||||
|
||||
&:not(.c-list-item__name) {
|
||||
color: $colorItemFgDetails;
|
||||
}
|
||||
|
||||
&.is-alias {
|
||||
|
@ -28,9 +28,5 @@
|
||||
padding-top: $p;
|
||||
padding-bottom: $p;
|
||||
width: 25%;
|
||||
|
||||
&:not(.c-list-item__name) {
|
||||
color: $colorItemFgDetails;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
33
src/plugins/folderView/components/status-listener.js
Normal file
33
src/plugins/folderView/components/status-listener.js
Normal 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();
|
||||
}
|
||||
};
|
@ -25,6 +25,8 @@ export default class GoToOriginalAction {
|
||||
this.name = 'Go To Original';
|
||||
this.key = 'goToOriginal';
|
||||
this.description = 'Go to the original unlinked instance of this object';
|
||||
this.group = 'action';
|
||||
this.priority = 4;
|
||||
|
||||
this._openmct = openmct;
|
||||
}
|
||||
|
@ -23,6 +23,6 @@ import GoToOriginalAction from './goToOriginalAction';
|
||||
|
||||
export default function () {
|
||||
return function (openmct) {
|
||||
openmct.contextMenu.registerAction(new GoToOriginalAction(openmct));
|
||||
openmct.actions.register(new GoToOriginalAction(openmct));
|
||||
};
|
||||
}
|
||||
|
@ -28,6 +28,8 @@ export default class NewFolderAction {
|
||||
this.key = 'newFolder';
|
||||
this.description = 'Create a new folder';
|
||||
this.cssClass = 'icon-folder-new';
|
||||
this.group = "action";
|
||||
this.priority = 9;
|
||||
|
||||
this._openmct = openmct;
|
||||
this._dialogForm = {
|
||||
|
@ -23,6 +23,6 @@ import NewFolderAction from './newFolderAction';
|
||||
|
||||
export default function () {
|
||||
return function (openmct) {
|
||||
openmct.contextMenu.registerAction(new NewFolderAction(openmct));
|
||||
openmct.actions.register(new NewFolderAction(openmct));
|
||||
};
|
||||
}
|
||||
|
@ -40,9 +40,7 @@ describe("the plugin", () => {
|
||||
openmct.on('start', done);
|
||||
openmct.startHeadless();
|
||||
|
||||
newFolderAction = openmct.contextMenu._allActions.filter(action => {
|
||||
return action.key === 'newFolder';
|
||||
})[0];
|
||||
newFolderAction = openmct.actions._allActions.newFolder;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
40
src/plugins/notebook/actions/CopyToNotebookAction.js
Normal file
40
src/plugins/notebook/actions/CopyToNotebookAction.js
Normal 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';
|
||||
}
|
||||
}
|
@ -111,7 +111,7 @@ import Search from '@/ui/components/search.vue';
|
||||
import SearchResults from './SearchResults.vue';
|
||||
import Sidebar from './Sidebar.vue';
|
||||
import { clearDefaultNotebook, getDefaultNotebook, setDefaultNotebook, setDefaultNotebookSection, setDefaultNotebookPage } from '../utils/notebook-storage';
|
||||
import { DEFAULT_CLASS, addNotebookEntry, createNewEmbed, getNotebookEntries, mutateObject } from '../utils/notebook-entries';
|
||||
import { addNotebookEntry, createNewEmbed, getNotebookEntries, mutateObject } from '../utils/notebook-entries';
|
||||
import objectUtils from 'objectUtils';
|
||||
|
||||
import { throttle } from 'lodash';
|
||||
@ -431,14 +431,7 @@ export default {
|
||||
return;
|
||||
}
|
||||
|
||||
const classList = domainObject.classList || [];
|
||||
const index = classList.indexOf(DEFAULT_CLASS);
|
||||
if (!classList.length || index < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
classList.splice(index, 1);
|
||||
mutateObject(this.openmct, domainObject, 'classList', classList);
|
||||
this.openmct.status.delete(domainObject.identifier);
|
||||
},
|
||||
searchItem(input) {
|
||||
this.search = input;
|
||||
|
@ -1,29 +1,17 @@
|
||||
<template>
|
||||
<div class="c-menu-button c-ctrl-wrapper c-ctrl-wrapper--menus-left">
|
||||
<button
|
||||
class="c-button--menu icon-notebook"
|
||||
class="c-icon-button c-button--menu icon-camera"
|
||||
title="Take a Notebook Snapshot"
|
||||
@click="setNotebookTypes"
|
||||
@click.stop="toggleMenu"
|
||||
@click.stop.prevent="showMenu"
|
||||
>
|
||||
<span class="c-button__label"></span>
|
||||
<span
|
||||
title="Take Notebook Snapshot"
|
||||
class="c-icon-button__label"
|
||||
>
|
||||
Snapshot
|
||||
</span>
|
||||
</button>
|
||||
<div
|
||||
v-show="showMenu"
|
||||
class="c-menu"
|
||||
>
|
||||
<ul>
|
||||
<li
|
||||
v-for="(type, index) in notebookTypes"
|
||||
:key="index"
|
||||
:class="type.cssClass"
|
||||
:title="type.name"
|
||||
@click="snapshot(type)"
|
||||
>
|
||||
{{ type.name }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -57,22 +45,20 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
notebookSnapshot: null,
|
||||
notebookTypes: [],
|
||||
showMenu: false
|
||||
notebookTypes: []
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.notebookSnapshot = new Snapshot(this.openmct);
|
||||
|
||||
document.addEventListener('click', this.hideMenu);
|
||||
},
|
||||
destroyed() {
|
||||
document.removeEventListener('click', this.hideMenu);
|
||||
this.setDefaultNotebookStatus();
|
||||
},
|
||||
methods: {
|
||||
setNotebookTypes() {
|
||||
showMenu(event) {
|
||||
const notebookTypes = [];
|
||||
const defaultNotebook = getDefaultNotebook();
|
||||
const elementBoundingClientRect = this.$el.getBoundingClientRect();
|
||||
const x = elementBoundingClientRect.x;
|
||||
const y = elementBoundingClientRect.y + elementBoundingClientRect.height;
|
||||
|
||||
if (defaultNotebook) {
|
||||
const domainObject = defaultNotebook.domainObject;
|
||||
@ -83,28 +69,24 @@ export default {
|
||||
notebookTypes.push({
|
||||
cssClass: 'icon-notebook',
|
||||
name: `Save to Notebook ${defaultPath}`,
|
||||
type: NOTEBOOK_DEFAULT
|
||||
callBack: () => {
|
||||
return this.snapshot(NOTEBOOK_DEFAULT);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
notebookTypes.push({
|
||||
cssClass: 'icon-notebook',
|
||||
cssClass: 'icon-camera',
|
||||
name: 'Save to Notebook Snapshots',
|
||||
type: NOTEBOOK_SNAPSHOT
|
||||
callBack: () => {
|
||||
return this.snapshot(NOTEBOOK_SNAPSHOT);
|
||||
}
|
||||
});
|
||||
|
||||
this.notebookTypes = notebookTypes;
|
||||
},
|
||||
toggleMenu() {
|
||||
this.showMenu = !this.showMenu;
|
||||
},
|
||||
hideMenu() {
|
||||
this.showMenu = false;
|
||||
this.openmct.menus.showMenu(x, y, notebookTypes);
|
||||
},
|
||||
snapshot(notebook) {
|
||||
this.hideMenu();
|
||||
|
||||
this.$nextTick(() => {
|
||||
const element = document.querySelector('.c-overlay__contents')
|
||||
|| document.getElementsByClassName('l-shell__main-container')[0];
|
||||
@ -124,6 +106,15 @@ export default {
|
||||
|
||||
this.notebookSnapshot.capture(snapshotMeta, notebook.type, element);
|
||||
});
|
||||
},
|
||||
setDefaultNotebookStatus() {
|
||||
let defaultNotebookObject = getDefaultNotebook();
|
||||
|
||||
if (defaultNotebookObject && defaultNotebookObject.notebookMeta) {
|
||||
let notebookIdentifier = defaultNotebookObject.notebookMeta.identifier;
|
||||
|
||||
this.openmct.status.set(notebookIdentifier, 'notebook-default');
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -4,7 +4,7 @@
|
||||
<div class="l-browse-bar__start">
|
||||
<div class="l-browse-bar__object-name--w">
|
||||
<div class="l-browse-bar__object-name c-object-label">
|
||||
<div class="c-object-label__type-icon icon-notebook"></div>
|
||||
<div class="c-object-label__type-icon icon-camera"></div>
|
||||
<div class="c-object-label__name">
|
||||
Notebook Snapshots
|
||||
<span v-if="snapshots.length"
|
||||
|
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="c-indicator c-indicator--clickable icon-notebook"
|
||||
<div class="c-indicator c-indicator--clickable icon-camera"
|
||||
:class="[
|
||||
{ 's-status-off': snapshotCount === 0 },
|
||||
{ 's-status-on': snapshotCount > 0 },
|
||||
|
@ -1,3 +1,4 @@
|
||||
import CopyToNotebookAction from './actions/CopyToNotebookAction';
|
||||
import Notebook from './components/Notebook.vue';
|
||||
import NotebookSnapshotIndicator from './components/NotebookSnapshotIndicator.vue';
|
||||
import SnapshotContainer from './snapshot-container';
|
||||
@ -13,6 +14,8 @@ export default function NotebookPlugin() {
|
||||
|
||||
installed = true;
|
||||
|
||||
openmct.actions.register(new CopyToNotebookAction(openmct));
|
||||
|
||||
const notebookType = {
|
||||
name: 'Notebook',
|
||||
description: 'Create and save timestamped notes with embedded object snapshots.',
|
||||
|
@ -20,7 +20,7 @@
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
import { createOpenMct, createMouseEvent, resetApplicationState } from 'utils/testing';
|
||||
import { createOpenMct, resetApplicationState } from 'utils/testing';
|
||||
import NotebookPlugin from './plugin';
|
||||
import Vue from 'vue';
|
||||
|
||||
@ -133,90 +133,4 @@ describe("Notebook plugin:", () => {
|
||||
expect(hasMajorElements).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Notebook Snapshots view:", () => {
|
||||
let snapshotIndicator;
|
||||
let drawerElement;
|
||||
|
||||
function clickSnapshotIndicator() {
|
||||
const indicator = element.querySelector('.icon-notebook');
|
||||
const button = indicator.querySelector('button');
|
||||
const clickEvent = createMouseEvent('click');
|
||||
|
||||
button.dispatchEvent(clickEvent);
|
||||
}
|
||||
|
||||
beforeAll(() => {
|
||||
snapshotIndicator = openmct.indicators.indicatorObjects
|
||||
.find(indicator => indicator.key === 'notebook-snapshot-indicator').element;
|
||||
|
||||
element.append(snapshotIndicator);
|
||||
|
||||
return Vue.nextTick();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
snapshotIndicator.remove();
|
||||
if (drawerElement) {
|
||||
drawerElement.remove();
|
||||
}
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
drawerElement = document.querySelector('.l-shell__drawer');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (drawerElement) {
|
||||
drawerElement.classList.remove('is-expanded');
|
||||
}
|
||||
});
|
||||
|
||||
it("has Snapshots indicator", () => {
|
||||
const hasSnapshotIndicator = snapshotIndicator !== null && snapshotIndicator !== undefined;
|
||||
expect(hasSnapshotIndicator).toBe(true);
|
||||
});
|
||||
|
||||
it("snapshots container has class isExpanded", () => {
|
||||
let classes = drawerElement.classList;
|
||||
const isExpandedBefore = classes.contains('is-expanded');
|
||||
|
||||
clickSnapshotIndicator();
|
||||
classes = drawerElement.classList;
|
||||
const isExpandedAfterFirstClick = classes.contains('is-expanded');
|
||||
|
||||
const success = isExpandedBefore === false
|
||||
&& isExpandedAfterFirstClick === true;
|
||||
|
||||
expect(success).toBe(true);
|
||||
});
|
||||
|
||||
it("snapshots container does not have class isExpanded", () => {
|
||||
let classes = drawerElement.classList;
|
||||
const isExpandedBefore = classes.contains('is-expanded');
|
||||
|
||||
clickSnapshotIndicator();
|
||||
classes = drawerElement.classList;
|
||||
const isExpandedAfterFirstClick = classes.contains('is-expanded');
|
||||
|
||||
clickSnapshotIndicator();
|
||||
classes = drawerElement.classList;
|
||||
const isExpandedAfterSecondClick = classes.contains('is-expanded');
|
||||
|
||||
const success = isExpandedBefore === false
|
||||
&& isExpandedAfterFirstClick === true
|
||||
&& isExpandedAfterSecondClick === false;
|
||||
|
||||
expect(success).toBe(true);
|
||||
});
|
||||
|
||||
it("show notebook snapshots container text", () => {
|
||||
clickSnapshotIndicator();
|
||||
|
||||
const notebookSnapshots = drawerElement.querySelector('.l-browse-bar__object-name');
|
||||
const snapshotsText = notebookSnapshots.textContent.trim();
|
||||
|
||||
expect(snapshotsText).toBe('Notebook Snapshots');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -49,7 +49,7 @@ export default class Snapshot {
|
||||
.then(domainObject => {
|
||||
addNotebookEntry(this.openmct, domainObject, notebookStorage, embed);
|
||||
|
||||
const defaultPath = `${domainObject.name} > ${notebookStorage.section.name} > ${notebookStorage.page.name}`;
|
||||
const defaultPath = `${domainObject.name} - ${notebookStorage.section.name} - ${notebookStorage.page.name}`;
|
||||
const msg = `Saved to Notebook ${defaultPath}`;
|
||||
this._showNotification(msg);
|
||||
});
|
||||
|
@ -1,5 +1,6 @@
|
||||
import objectLink from '../../../ui/mixins/object-link';
|
||||
export const DEFAULT_CLASS = 'is-notebook-default';
|
||||
|
||||
export const DEFAULT_CLASS = 'notebook-default';
|
||||
const TIME_BOUNDS = {
|
||||
START_BOUND: 'tc.startBound',
|
||||
END_BOUND: 'tc.endBound',
|
||||
@ -96,7 +97,7 @@ export function createNewEmbed(snapshotMeta, snapshot = '') {
|
||||
};
|
||||
}
|
||||
|
||||
export function addNotebookEntry(openmct, domainObject, notebookStorage, embed = null) {
|
||||
export function addNotebookEntry(openmct, domainObject, notebookStorage, embed = null, entryText = '') {
|
||||
if (!openmct || !domainObject || !notebookStorage) {
|
||||
return;
|
||||
}
|
||||
@ -117,14 +118,14 @@ export function addNotebookEntry(openmct, domainObject, notebookStorage, embed =
|
||||
const entry = {
|
||||
id,
|
||||
createdOn: date,
|
||||
text: '',
|
||||
text: entryText,
|
||||
embeds
|
||||
};
|
||||
|
||||
const newEntries = addEntryIntoPage(notebookStorage, entries, entry);
|
||||
|
||||
addDefaultClass(domainObject);
|
||||
mutateObject(openmct, domainObject, 'configuration.entries', newEntries);
|
||||
addDefaultClass(domainObject, openmct);
|
||||
openmct.objects.mutate(domainObject, 'configuration.entries', newEntries);
|
||||
|
||||
return id;
|
||||
}
|
||||
@ -197,13 +198,6 @@ export function mutateObject(openmct, object, key, value) {
|
||||
openmct.objects.mutate(object, key, value);
|
||||
}
|
||||
|
||||
function addDefaultClass(domainObject) {
|
||||
const classList = domainObject.classList || [];
|
||||
if (classList.includes(DEFAULT_CLASS)) {
|
||||
return;
|
||||
}
|
||||
|
||||
classList.push(DEFAULT_CLASS);
|
||||
|
||||
domainObject.classList = classList;
|
||||
function addDefaultClass(domainObject, openmct) {
|
||||
openmct.status.set(domainObject.identifier, DEFAULT_CLASS);
|
||||
}
|
||||
|
@ -40,7 +40,7 @@
|
||||
<div class="c-state-indicator__alert-cursor-lock icon-cursor-lock" title="Cursor is point locked. Click anywhere in the plot to unlock."></div>
|
||||
<div class="plot-legend-item"
|
||||
ng-class="{
|
||||
'is-missing': series.domainObject.status === 'missing'
|
||||
'is-status--missing': series.domainObject.status === 'missing'
|
||||
}"
|
||||
ng-repeat="series in series track by $index"
|
||||
>
|
||||
@ -48,7 +48,7 @@
|
||||
<span class="plot-series-color-swatch"
|
||||
ng-style="{ 'background-color': series.get('color').asHexString() }">
|
||||
</span>
|
||||
<span class="is-missing__indicator" title="This item is missing"></span>
|
||||
<span class="is-status__indicator" title="This item is missing or suspect"></span>
|
||||
<span class="plot-series-name">{{ series.nameWithUnit() }}</span>
|
||||
</div>
|
||||
<div class="plot-series-value hover-value-enabled value-to-display-{{ legend.get('valueToShowWhenCollapsed') }} {{ series.closest.mctLimitState.cssClass }}"
|
||||
@ -95,14 +95,14 @@
|
||||
<tr ng-repeat="series in series"
|
||||
class="plot-legend-item"
|
||||
ng-class="{
|
||||
'is-missing': series.domainObject.status === 'missing'
|
||||
'is-status--missing': series.domainObject.status === 'missing'
|
||||
}"
|
||||
>
|
||||
<td class="plot-series-swatch-and-name">
|
||||
<span class="plot-series-color-swatch"
|
||||
ng-style="{ 'background-color': series.get('color').asHexString() }">
|
||||
</span>
|
||||
<span class="is-missing__indicator" title="This item is missing"></span>
|
||||
<span class="is-status__indicator" title="This item is missing or suspect"></span>
|
||||
<span class="plot-series-name">{{ series.get('name') }}</span>
|
||||
</td>
|
||||
|
||||
|
@ -58,7 +58,8 @@ define([
|
||||
'./newFolderAction/plugin',
|
||||
'./persistence/couch/plugin',
|
||||
'./defaultRootName/plugin',
|
||||
'./timeline/plugin'
|
||||
'./timeline/plugin',
|
||||
'./viewDatumAction/plugin'
|
||||
], function (
|
||||
_,
|
||||
UTCTimeSystem,
|
||||
@ -97,7 +98,8 @@ define([
|
||||
NewFolderAction,
|
||||
CouchDBPlugin,
|
||||
DefaultRootName,
|
||||
Timeline
|
||||
Timeline,
|
||||
ViewDatumAction
|
||||
) {
|
||||
const bundleMap = {
|
||||
LocalStorage: 'platform/persistence/local',
|
||||
@ -191,6 +193,7 @@ define([
|
||||
plugins.ISOTimeFormat = ISOTimeFormat.default;
|
||||
plugins.DefaultRootName = DefaultRootName.default;
|
||||
plugins.Timeline = Timeline.default;
|
||||
plugins.ViewDatumAction = ViewDatumAction.default;
|
||||
|
||||
return plugins;
|
||||
});
|
||||
|
@ -25,6 +25,8 @@ export default class RemoveAction {
|
||||
this.key = 'remove';
|
||||
this.description = 'Remove this object from its containing object.';
|
||||
this.cssClass = "icon-trash";
|
||||
this.group = "action";
|
||||
this.priority = 1;
|
||||
|
||||
this.openmct = openmct;
|
||||
}
|
||||
@ -103,6 +105,16 @@ export default class RemoveAction {
|
||||
let parentType = parent && this.openmct.types.get(parent.type);
|
||||
let child = objectPath[0];
|
||||
let locked = child.locked ? child.locked : parent && parent.locked;
|
||||
let isEditing = this.openmct.editor.isEditing();
|
||||
|
||||
if (isEditing) {
|
||||
let currentItemInView = this.openmct.router.path[0];
|
||||
let domainObject = objectPath[0];
|
||||
|
||||
if (this.openmct.objects.areIdsEqual(currentItemInView.identifier, domainObject.identifier)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (locked) {
|
||||
return false;
|
||||
|
@ -23,6 +23,6 @@ import RemoveAction from "./RemoveAction";
|
||||
|
||||
export default function () {
|
||||
return function (openmct) {
|
||||
openmct.contextMenu.registerAction(new RemoveAction(openmct));
|
||||
openmct.actions.register(new RemoveAction(openmct));
|
||||
};
|
||||
}
|
||||
|
@ -20,8 +20,8 @@
|
||||
Drag objects here to add them to this view.
|
||||
</div>
|
||||
<div
|
||||
v-for="(tab,index) in tabsList"
|
||||
:key="index"
|
||||
v-for="(tab, index) in tabsList"
|
||||
:key="tab.keyString"
|
||||
class="c-tab c-tabs-view__tab"
|
||||
:class="{
|
||||
'is-current': isCurrent(tab)
|
||||
@ -29,13 +29,13 @@
|
||||
@click="showTab(tab, index)"
|
||||
>
|
||||
<div class="c-tabs-view__tab__label c-object-label"
|
||||
:class="{'is-missing': tab.domainObject.status === 'missing'}"
|
||||
:class="[tab.status ? `is-status--${tab.status}` : '']"
|
||||
>
|
||||
<div class="c-object-label__type-icon"
|
||||
:class="tab.type.definition.cssClass"
|
||||
>
|
||||
<span class="is-missing__indicator"
|
||||
title="This item is missing"
|
||||
<span class="is-status__indicator"
|
||||
:title="`This item is ${tab.status}`"
|
||||
></span>
|
||||
</div>
|
||||
<span class="c-button__label c-object-label__name">{{ tab.domainObject.name }}</span>
|
||||
@ -47,8 +47,8 @@
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-for="(tab, index) in tabsList"
|
||||
:key="index"
|
||||
v-for="tab in tabsList"
|
||||
:key="tab.keyString"
|
||||
class="c-tabs-view__object-holder"
|
||||
:class="{'c-tabs-view__object-holder--hidden': !isCurrent(tab)}"
|
||||
>
|
||||
@ -56,6 +56,7 @@
|
||||
v-if="internalDomainObject.keep_alive ? currentTab : isCurrent(tab)"
|
||||
class="c-tabs-view__object"
|
||||
:object="tab.domainObject"
|
||||
:object-path="tab.objectPath"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@ -78,7 +79,7 @@ const unknownObjectType = {
|
||||
};
|
||||
|
||||
export default {
|
||||
inject: ['openmct', 'domainObject', 'composition'],
|
||||
inject: ['openmct', 'domainObject', 'composition', 'objectPath'],
|
||||
components: {
|
||||
ObjectView
|
||||
},
|
||||
@ -139,6 +140,10 @@ export default {
|
||||
this.composition.off('remove', this.removeItem);
|
||||
this.composition.off('reorder', this.onReorder);
|
||||
|
||||
this.tabsList.forEach(tab => {
|
||||
tab.statusUnsubscribe();
|
||||
});
|
||||
|
||||
this.unsubscribe();
|
||||
this.clearCurrentTabIndexFromURL();
|
||||
|
||||
@ -192,10 +197,19 @@ export default {
|
||||
},
|
||||
addItem(domainObject) {
|
||||
let type = this.openmct.types.get(domainObject.type) || unknownObjectType;
|
||||
let keyString = this.openmct.objects.makeKeyString(domainObject.identifier);
|
||||
let status = this.openmct.status.get(domainObject.identifier);
|
||||
let statusUnsubscribe = this.openmct.status.observe(keyString, (updatedStatus) => {
|
||||
this.updateStatus(keyString, updatedStatus);
|
||||
});
|
||||
let objectPath = [domainObject].concat(this.objectPath.slice());
|
||||
let tabItem = {
|
||||
domainObject,
|
||||
type: type,
|
||||
key: this.openmct.objects.makeKeyString(domainObject.identifier)
|
||||
status,
|
||||
statusUnsubscribe,
|
||||
objectPath,
|
||||
type,
|
||||
keyString
|
||||
};
|
||||
|
||||
this.tabsList.push(tabItem);
|
||||
@ -211,10 +225,12 @@ export default {
|
||||
},
|
||||
removeItem(identifier) {
|
||||
let pos = this.tabsList.findIndex(tab =>
|
||||
tab.domainObject.identifier.namespace === identifier.namespace && tab.domainObject.identifier.key === identifier.key
|
||||
tab.domainObject.identifier.namespace === identifier.namespace && tab.domainObject.identifier.keyString === identifier.keyString
|
||||
);
|
||||
let tabToBeRemoved = this.tabsList[pos];
|
||||
|
||||
tabToBeRemoved.statusUnsubscribe();
|
||||
|
||||
this.tabsList.splice(pos, 1);
|
||||
|
||||
if (this.isCurrent(tabToBeRemoved)) {
|
||||
@ -252,7 +268,7 @@ export default {
|
||||
this.allowDrop = false;
|
||||
},
|
||||
isCurrent(tab) {
|
||||
return this.currentTab.key === tab.key;
|
||||
return this.currentTab.keyString === tab.keyString;
|
||||
},
|
||||
updateInternalDomainObject(domainObject) {
|
||||
this.internalDomainObject = domainObject;
|
||||
@ -270,6 +286,16 @@ export default {
|
||||
},
|
||||
clearCurrentTabIndexFromURL() {
|
||||
deleteSearchParam(this.searchTabKey);
|
||||
},
|
||||
updateStatus(keyString, status) {
|
||||
let tabPos = this.tabsList.findIndex((tab) => {
|
||||
return tab.keyString === keyString;
|
||||
});
|
||||
|
||||
if (tabPos !== -1) {
|
||||
let tab = this.tabsList[tabPos];
|
||||
this.$set(tab, 'status', status);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -38,7 +38,7 @@ define([
|
||||
canEdit: function (domainObject) {
|
||||
return domainObject.type === 'tabs';
|
||||
},
|
||||
view: function (domainObject) {
|
||||
view: function (domainObject, objectPath) {
|
||||
let component;
|
||||
|
||||
return {
|
||||
@ -56,6 +56,7 @@ define([
|
||||
provide: {
|
||||
openmct,
|
||||
domainObject,
|
||||
objectPath,
|
||||
composition: openmct.composition.get(domainObject)
|
||||
},
|
||||
template: '<tabs-component :isEditing="isEditing"></tabs-component>'
|
||||
|
@ -180,7 +180,6 @@ define([
|
||||
processHistoricalData(telemetryData, columnMap, keyString, limitEvaluator) {
|
||||
let telemetryRows = telemetryData.map(datum => new TelemetryTableRow(datum, columnMap, keyString, limitEvaluator));
|
||||
this.boundedRows.add(telemetryRows);
|
||||
this.emit('historical-rows-processed');
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -26,6 +26,7 @@ define([], function () {
|
||||
this.columns = columns;
|
||||
|
||||
this.datum = createNormalizedDatum(datum, columns);
|
||||
this.fullDatum = datum;
|
||||
this.limitEvaluator = limitEvaluator;
|
||||
this.objectKeyString = objectKeyString;
|
||||
}
|
||||
@ -87,7 +88,7 @@ define([], function () {
|
||||
}
|
||||
|
||||
getContextMenuActions() {
|
||||
return [];
|
||||
return ['viewDatumAction'];
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -54,15 +54,13 @@ define([
|
||||
view(domainObject, objectPath) {
|
||||
let table = new TelemetryTable(domainObject, openmct);
|
||||
let component;
|
||||
|
||||
let markingProp = {
|
||||
enable: true,
|
||||
useAlternateControlBar: false,
|
||||
rowName: '',
|
||||
rowNamePlural: ''
|
||||
};
|
||||
|
||||
return {
|
||||
const view = {
|
||||
show: function (element, editMode) {
|
||||
component = new Vue({
|
||||
el: element,
|
||||
@ -72,7 +70,8 @@ define([
|
||||
data() {
|
||||
return {
|
||||
isEditing: editMode,
|
||||
markingProp
|
||||
markingProp,
|
||||
view
|
||||
};
|
||||
},
|
||||
provide: {
|
||||
@ -80,7 +79,7 @@ define([
|
||||
table,
|
||||
objectPath
|
||||
},
|
||||
template: '<table-component :isEditing="isEditing" :marking="markingProp"/>'
|
||||
template: '<table-component ref="tableComponent" :isEditing="isEditing" :marking="markingProp" :view="view"/>'
|
||||
});
|
||||
},
|
||||
onEditModeChange(editMode) {
|
||||
@ -89,11 +88,22 @@ define([
|
||||
onClearData() {
|
||||
table.clearData();
|
||||
},
|
||||
getViewContext() {
|
||||
if (component) {
|
||||
return component.$refs.tableComponent.getViewContext();
|
||||
} else {
|
||||
return {
|
||||
type: 'telemetry-table'
|
||||
};
|
||||
}
|
||||
},
|
||||
destroy: function (element) {
|
||||
component.$destroy();
|
||||
component = undefined;
|
||||
}
|
||||
};
|
||||
|
||||
return view;
|
||||
},
|
||||
priority() {
|
||||
return 1;
|
||||
|
123
src/plugins/telemetryTable/ViewActions.js
Normal file
123
src/plugins/telemetryTable/ViewActions.js
Normal 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;
|
@ -102,7 +102,16 @@ export default {
|
||||
selectable[columnKeys] = this.row.columns[columnKeys].selectable;
|
||||
|
||||
return selectable;
|
||||
}, {})
|
||||
}, {}),
|
||||
actionsViewContext: {
|
||||
getViewContext: () => {
|
||||
return {
|
||||
viewHistoricalData: true,
|
||||
viewDatumAction: true,
|
||||
getDatum: this.getDatum
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@ -170,14 +179,25 @@ export default {
|
||||
event.stopPropagation();
|
||||
}
|
||||
},
|
||||
getDatum() {
|
||||
return this.row.fullDatum;
|
||||
},
|
||||
showContextMenu: function (event) {
|
||||
event.preventDefault();
|
||||
|
||||
this.markRow(event);
|
||||
|
||||
this.row.getContextualDomainObject(this.openmct, this.row.objectKeyString).then(domainObject => {
|
||||
let contextualObjectPath = this.objectPath.slice();
|
||||
contextualObjectPath.unshift(domainObject);
|
||||
|
||||
this.openmct.contextMenu._showContextMenuForObjectPath(contextualObjectPath, event.x, event.y, this.row.getContextMenuActions());
|
||||
let actionsCollection = this.openmct.actions.get(contextualObjectPath, this.actionsViewContext);
|
||||
let allActions = actionsCollection.getActionsObject();
|
||||
let applicableActions = this.row.getContextMenuActions().map(key => allActions[key]);
|
||||
|
||||
if (applicableActions.length) {
|
||||
this.openmct.menus.showMenu(event.x, event.y, applicableActions);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -23,8 +23,7 @@
|
||||
<div class="c-table-wrapper"
|
||||
:class="{ 'is-paused': paused }"
|
||||
>
|
||||
<!-- main contolbar start-->
|
||||
<div v-if="!marking.useAlternateControlBar"
|
||||
<div v-if="enableLegacyToolbar"
|
||||
class="c-table-control-bar c-control-bar"
|
||||
>
|
||||
<button
|
||||
@ -94,7 +93,6 @@
|
||||
|
||||
<slot name="buttons"></slot>
|
||||
</div>
|
||||
<!-- main controlbar end -->
|
||||
|
||||
<!-- alternate controlbar start -->
|
||||
<div v-if="marking.useAlternateControlBar"
|
||||
@ -113,11 +111,11 @@
|
||||
|
||||
<button
|
||||
:class="{'hide-nice': !markedRows.length}"
|
||||
class="c-button icon-x labeled"
|
||||
class="c-icon-button icon-x labeled"
|
||||
title="Deselect All"
|
||||
@click="unmarkAllRows()"
|
||||
>
|
||||
<span class="c-button__label">{{ `Deselect ${marking.disableMultiSelect ? '' : 'All'}` }} </span>
|
||||
<span class="c-icon-button__label">{{ `Deselect ${marking.disableMultiSelect ? '' : 'All'}` }} </span>
|
||||
</button>
|
||||
|
||||
<slot name="buttons"></slot>
|
||||
@ -301,12 +299,12 @@ export default {
|
||||
default: true
|
||||
},
|
||||
allowFiltering: {
|
||||
'type': Boolean,
|
||||
'default': true
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
allowSorting: {
|
||||
'type': Boolean,
|
||||
'default': true
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
marking: {
|
||||
type: Object,
|
||||
@ -319,6 +317,17 @@ export default {
|
||||
rowNamePlural: ""
|
||||
};
|
||||
}
|
||||
},
|
||||
enableLegacyToolbar: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
view: {
|
||||
type: Object,
|
||||
required: false,
|
||||
default() {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
@ -394,6 +403,40 @@ export default {
|
||||
markedRows: {
|
||||
handler(newVal, oldVal) {
|
||||
this.$emit('marked-rows-updated', newVal, oldVal);
|
||||
|
||||
if (this.viewActionsCollection) {
|
||||
if (newVal.length > 0) {
|
||||
this.viewActionsCollection.enable(['export-csv-marked', 'unmark-all-rows']);
|
||||
} else if (newVal.length === 0) {
|
||||
this.viewActionsCollection.disable(['export-csv-marked', 'unmark-all-rows']);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
paused: {
|
||||
handler(newVal) {
|
||||
if (this.viewActionsCollection) {
|
||||
if (newVal) {
|
||||
this.viewActionsCollection.hide(['pause-data']);
|
||||
this.viewActionsCollection.show(['play-data']);
|
||||
} else {
|
||||
this.viewActionsCollection.hide(['play-data']);
|
||||
this.viewActionsCollection.show(['pause-data']);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
isAutosizeEnabled: {
|
||||
handler(newVal) {
|
||||
if (this.viewActionsCollection) {
|
||||
if (newVal) {
|
||||
this.viewActionsCollection.show(['expand-columns']);
|
||||
this.viewActionsCollection.hide(['autosize-columns']);
|
||||
} else {
|
||||
this.viewActionsCollection.show(['autosize-columns']);
|
||||
this.viewActionsCollection.hide(['expand-columns']);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -406,6 +449,11 @@ export default {
|
||||
this.rowsRemoved = _.throttle(this.rowsRemoved, 200);
|
||||
this.scroll = _.throttle(this.scroll, 100);
|
||||
|
||||
if (!this.marking.useAlternateControlBar && !this.enableLegacyToolbar) {
|
||||
this.viewActionsCollection = this.openmct.actions.get(this.objectPath, this.view);
|
||||
this.initializeViewActions();
|
||||
}
|
||||
|
||||
this.table.on('object-added', this.addObject);
|
||||
this.table.on('object-removed', this.removeObject);
|
||||
this.table.on('outstanding-requests', this.outstandingRequests);
|
||||
@ -846,7 +894,7 @@ export default {
|
||||
|
||||
for (let i = firstRowIndex; i <= lastRowIndex; i++) {
|
||||
let row = allRows[i];
|
||||
row.marked = true;
|
||||
this.$set(row, 'marked', true);
|
||||
|
||||
if (row !== baseRow) {
|
||||
this.markedRows.push(row);
|
||||
@ -908,6 +956,40 @@ export default {
|
||||
|
||||
this.$nextTick().then(this.calculateColumnWidths);
|
||||
},
|
||||
getViewContext() {
|
||||
return {
|
||||
type: 'telemetry-table',
|
||||
exportAllDataAsCSV: this.exportAllDataAsCSV,
|
||||
exportMarkedRows: this.exportMarkedRows,
|
||||
unmarkAllRows: this.unmarkAllRows,
|
||||
togglePauseByButton: this.togglePauseByButton,
|
||||
expandColumns: this.recalculateColumnWidths,
|
||||
autosizeColumns: this.autosizeColumns
|
||||
};
|
||||
},
|
||||
initializeViewActions() {
|
||||
if (this.markedRows.length > 0) {
|
||||
this.viewActionsCollection.enable(['export-csv-marked', 'unmark-all-rows']);
|
||||
} else if (this.markedRows.length === 0) {
|
||||
this.viewActionsCollection.disable(['export-csv-marked', 'unmark-all-rows']);
|
||||
}
|
||||
|
||||
if (this.paused) {
|
||||
this.viewActionsCollection.hide(['pause-data']);
|
||||
this.viewActionsCollection.show(['play-data']);
|
||||
} else {
|
||||
this.viewActionsCollection.hide(['play-data']);
|
||||
this.viewActionsCollection.show(['pause-data']);
|
||||
}
|
||||
|
||||
if (this.isAutosizeEnabled) {
|
||||
this.viewActionsCollection.show(['expand-columns']);
|
||||
this.viewActionsCollection.hide(['autosize-columns']);
|
||||
} else {
|
||||
this.viewActionsCollection.show(['autosize-columns']);
|
||||
this.viewActionsCollection.hide(['expand-columns']);
|
||||
}
|
||||
},
|
||||
setRowHeight(height) {
|
||||
this.rowHeight = height;
|
||||
this.setHeight();
|
||||
|
@ -23,11 +23,13 @@
|
||||
define([
|
||||
'./TelemetryTableViewProvider',
|
||||
'./TableConfigurationViewProvider',
|
||||
'./TelemetryTableType'
|
||||
'./TelemetryTableType',
|
||||
'./ViewActions'
|
||||
], function (
|
||||
TelemetryTableViewProvider,
|
||||
TableConfigurationViewProvider,
|
||||
TelemetryTableType
|
||||
TelemetryTableType,
|
||||
TelemetryTableViewActions
|
||||
) {
|
||||
return function plugin() {
|
||||
return function install(openmct) {
|
||||
@ -41,6 +43,10 @@ define([
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
TelemetryTableViewActions.default.forEach(action => {
|
||||
openmct.actions.register(action);
|
||||
});
|
||||
};
|
||||
};
|
||||
});
|
||||
|
@ -168,6 +168,8 @@ describe("the plugin", () => {
|
||||
return telemetryPromise;
|
||||
});
|
||||
|
||||
openmct.router.path = [testTelemetryObject];
|
||||
|
||||
applicableViews = openmct.objectViews.get(testTelemetryObject);
|
||||
tableViewProvider = applicableViews.find((viewProvider) => viewProvider.key === 'table');
|
||||
tableView = tableViewProvider.view(testTelemetryObject, [testTelemetryObject]);
|
||||
|
69
src/plugins/viewDatumAction/ViewDatumAction.js
Normal file
69
src/plugins/viewDatumAction/ViewDatumAction.js
Normal 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;
|
||||
}
|
||||
}
|
24
src/plugins/viewDatumAction/components/MetadataList.vue
Normal file
24
src/plugins/viewDatumAction/components/MetadataList.vue
Normal 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>
|
30
src/plugins/viewDatumAction/components/metadata-list.scss
Normal file
30
src/plugins/viewDatumAction/components/metadata-list.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
29
src/plugins/viewDatumAction/plugin.js
Normal file
29
src/plugins/viewDatumAction/plugin.js
Normal 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));
|
||||
};
|
||||
}
|
95
src/plugins/viewDatumAction/pluginSpec.js
Normal file
95
src/plugins/viewDatumAction/pluginSpec.js
Normal 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();
|
||||
});
|
||||
});
|
||||
});
|
@ -218,7 +218,7 @@ $colorBtnActiveFg: $colorOkFg;
|
||||
$colorBtnSelectedBg: $colorSelectedBg;
|
||||
$colorBtnSelectedFg: $colorSelectedFg;
|
||||
$colorClickIconButton: $colorKey;
|
||||
$colorClickIconButtonBgHov: rgba($colorKey, 0.6);
|
||||
$colorClickIconButtonBgHov: rgba($colorKey, 0.3);
|
||||
$colorClickIconButtonFgHov: $colorKeyHov;
|
||||
$colorDropHint: $colorKey;
|
||||
$colorDropHintBg: pushBack($colorDropHint, 10%);
|
||||
@ -378,6 +378,11 @@ $colorItemTreeVC: $colorDisclosureCtrl;
|
||||
$colorItemTreeVCHover: $colorDisclosureCtrlHov;
|
||||
$shdwItemTreeIcon: none;
|
||||
|
||||
// Layout frame controls
|
||||
$frameControlsColorFg: white;
|
||||
$frameControlsColorBg: $colorKey;
|
||||
$frameControlsShdw: $shdwMenu;
|
||||
|
||||
// Images
|
||||
$colorThumbHoverBg: $colorItemTreeHoverBg;
|
||||
|
||||
|
@ -382,6 +382,11 @@ $colorItemTreeVC: $colorDisclosureCtrl;
|
||||
$colorItemTreeVCHover: $colorDisclosureCtrlHov;
|
||||
$shdwItemTreeIcon: none;
|
||||
|
||||
// Layout frame controls
|
||||
$frameControlsColorFg: white;
|
||||
$frameControlsColorBg: $colorKey;
|
||||
$frameControlsShdw: $shdwMenu;
|
||||
|
||||
// Images
|
||||
$colorThumbHoverBg: $colorItemTreeHoverBg;
|
||||
|
||||
|
@ -80,13 +80,13 @@ $uiColor: #289fec; // Resize bars, splitter bars, etc.
|
||||
$colorInteriorBorder: rgba($colorBodyFg, 0.2);
|
||||
$colorA: $colorBodyFg;
|
||||
$colorAHov: $colorKey;
|
||||
$filterHov: brightness(0.8) contrast(2); // Tree, location items
|
||||
$colorSelectedBg: rgba($colorKey, 0.2);
|
||||
$filterHov: hue-rotate(-10deg) brightness(0.8) contrast(2); // Tree, location items
|
||||
$colorSelectedBg: pushBack($colorKey, 40%);
|
||||
$colorSelectedFg: pullForward($colorBodyFg, 10%);
|
||||
|
||||
// Object labels
|
||||
$objectLabelTypeIconOpacity: 0.5;
|
||||
$objectLabelNameFilter: brightness(0.5);
|
||||
$objectLabelNameFilter: brightness(0.9);
|
||||
|
||||
// Layout
|
||||
$shellMainPad: 4px 0;
|
||||
@ -378,6 +378,11 @@ $colorItemTreeVC: $colorDisclosureCtrl;
|
||||
$colorItemTreeVCHover: $colorDisclosureCtrlHov;
|
||||
$shdwItemTreeIcon: none;
|
||||
|
||||
// Layout frame controls
|
||||
$frameControlsColorFg: $colorClickIconButton;
|
||||
$frameControlsColorBg: $colorMenuBg;
|
||||
$frameControlsShdw: $shdwMenu;
|
||||
|
||||
// Images
|
||||
$colorThumbHoverBg: $colorItemTreeHoverBg;
|
||||
|
||||
|
@ -512,7 +512,7 @@ select {
|
||||
|
||||
&__section-hint {
|
||||
$m: $interiorMargin;
|
||||
margin: $m 0;
|
||||
margin: 0 0 $m 0;
|
||||
padding: $m nth($menuItemPad, 2) 0 nth($menuItemPad, 2);
|
||||
|
||||
opacity: 0.6;
|
||||
@ -524,7 +524,7 @@ select {
|
||||
$m: $interiorMargin;
|
||||
border-top: 1px solid $colorInteriorBorder;
|
||||
margin: $m 0;
|
||||
padding: $m nth($menuItemPad, 2) 0 nth($menuItemPad, 2);
|
||||
padding: 0 nth($menuItemPad, 2) 0 nth($menuItemPad, 2);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -59,14 +59,8 @@ mct-plot {
|
||||
}
|
||||
|
||||
/*********************** MISSING ITEM INDICATORS */
|
||||
.is-missing__indicator {
|
||||
display: none;
|
||||
}
|
||||
.is-missing {
|
||||
@include isMissing();
|
||||
.is-missing__indicator {
|
||||
font-size: 0.8em;
|
||||
}
|
||||
.is-status__indicator {
|
||||
font-size: 0.8em;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -129,31 +129,21 @@
|
||||
}
|
||||
}
|
||||
|
||||
@mixin isMissing($absPos: false) {
|
||||
@mixin isStatus($absPos: false) {
|
||||
// Supports CSS classing as follows:
|
||||
// is-status--missing, is-status--suspect, etc.
|
||||
// Common styles to be applied to tree items, object labels, grid and list item views
|
||||
//opacity: 0.7;
|
||||
//pointer-events: none; // Don't think we can do this, as disables title hover on icon element
|
||||
|
||||
.is-missing__indicator {
|
||||
display: none ;
|
||||
.is-status__indicator {
|
||||
display: block ; // Set to display: none in status.scss
|
||||
text-shadow: $colorBodyBg 0 0 2px;
|
||||
color: $colorAlert;
|
||||
font-family: symbolsfont;
|
||||
|
||||
&:before {
|
||||
content: $glyph-icon-alert-triangle;
|
||||
}
|
||||
}
|
||||
|
||||
@if $absPos {
|
||||
.is-missing__indicator {
|
||||
@if $absPos {
|
||||
position: absolute;
|
||||
z-index: 3;
|
||||
}
|
||||
}
|
||||
|
||||
&.is-missing .is-missing__indicator,
|
||||
.is-missing .is-missing__indicator { display: block !important; }
|
||||
}
|
||||
|
||||
@mixin bgDiagonalStripes($c: yellow, $a: 0.1, $d: 40px) {
|
||||
@ -502,8 +492,8 @@
|
||||
}
|
||||
|
||||
@mixin cClickIconButtonLayout() {
|
||||
$pLR: 4px;
|
||||
$pTB: 4px;
|
||||
$pLR: 5px;
|
||||
$pTB: 5px;
|
||||
padding: $pTB $pLR;
|
||||
|
||||
&:before,
|
||||
@ -522,6 +512,7 @@
|
||||
@include cControl();
|
||||
@include cClickIconButtonLayout();
|
||||
background: none;
|
||||
color: $colorClickIconButton;
|
||||
box-shadow: none;
|
||||
cursor: pointer;
|
||||
transition: $transOut;
|
||||
@ -530,7 +521,8 @@
|
||||
@include hover() {
|
||||
transition: $transIn;
|
||||
background: $colorClickIconButtonBgHov;
|
||||
color: $colorClickIconButtonFgHov;
|
||||
//color: $colorClickIconButtonFgHov;
|
||||
filter: $filterHov;
|
||||
}
|
||||
|
||||
&[class*="--major"] {
|
||||
|
@ -198,3 +198,27 @@ tr {
|
||||
|
||||
.u-alert { @include uIndicator($colorAlert, $colorAlertFg, $glyph-icon-alert-triangle); }
|
||||
.u-error { @include uIndicator($colorError, $colorErrorFg, $glyph-icon-alert-triangle); }
|
||||
|
||||
.is-status {
|
||||
&__indicator {
|
||||
display: none; // Default state; is set to block when within an actual is-status class
|
||||
}
|
||||
|
||||
&--missing {
|
||||
@include isStatus();
|
||||
|
||||
.is-status__indicator:before {
|
||||
color: $colorAlert;
|
||||
content: $glyph-icon-alert-triangle;
|
||||
}
|
||||
}
|
||||
|
||||
&--suspect {
|
||||
@include isStatus();
|
||||
|
||||
.is-status__indicator:before {
|
||||
color: $colorWarningLo;
|
||||
content: $glyph-icon-alert-rect;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -97,6 +97,7 @@ div.c-table {
|
||||
}
|
||||
}
|
||||
.c-table-control-bar {
|
||||
.c-icon-button,
|
||||
.c-click-icon,
|
||||
.c-button {
|
||||
&__label {
|
||||
|
@ -213,7 +213,8 @@
|
||||
}
|
||||
}
|
||||
|
||||
.is-notebook-default {
|
||||
.is-notebook-default,
|
||||
.is-status--notebook-default {
|
||||
&:after {
|
||||
color: $colorFilter;
|
||||
content: $glyph-icon-notebook-page;
|
||||
|
@ -27,6 +27,7 @@
|
||||
@import "../plugins/timeConductor/conductor-mode-icon.scss";
|
||||
@import "../plugins/timeConductor/date-picker.scss";
|
||||
@import "../plugins/timeline/timeline-axis.scss";
|
||||
@import "../plugins/viewDatumAction/components/metadata-list.scss";
|
||||
@import "../ui/components/object-frame.scss";
|
||||
@import "../ui/components/object-label.scss";
|
||||
@import "../ui/components/progress-bar.scss";
|
||||
|
@ -21,45 +21,70 @@
|
||||
*****************************************************************************/
|
||||
<template>
|
||||
<div
|
||||
class="c-so-view has-local-controls"
|
||||
:class="{
|
||||
'c-so-view--no-frame': !hasFrame,
|
||||
'has-complex-content': complexContent,
|
||||
'is-missing': domainObject.status === 'missing'
|
||||
}"
|
||||
class="c-so-view"
|
||||
:class="[
|
||||
statusClass,
|
||||
'c-so-view--' + domainObject.type,
|
||||
{
|
||||
'c-so-view--no-frame': !hasFrame,
|
||||
'has-complex-content': complexContent
|
||||
}
|
||||
]"
|
||||
>
|
||||
<div class="c-so-view__header">
|
||||
<div
|
||||
class="c-so-view__header"
|
||||
>
|
||||
<div class="c-object-label"
|
||||
:class="{
|
||||
classList,
|
||||
'is-missing': domainObject.status === 'missing'
|
||||
}"
|
||||
:class="[ statusClass ]"
|
||||
>
|
||||
<div class="c-object-label__type-icon"
|
||||
:class="cssClass"
|
||||
>
|
||||
<span class="is-missing__indicator"
|
||||
title="This item is missing"
|
||||
<span class="is-status__indicator"
|
||||
:title="`This item is ${status}`"
|
||||
></span>
|
||||
</div>
|
||||
<div class="c-object-label__name">
|
||||
{{ domainObject && domainObject.name }}
|
||||
</div>
|
||||
</div>
|
||||
<context-menu-drop-down
|
||||
:object-path="objectPath"
|
||||
/>
|
||||
|
||||
<div
|
||||
class="c-so-view__frame-controls"
|
||||
:class="{
|
||||
'c-so-view__frame-controls--no-frame': !hasFrame,
|
||||
'has-complex-content': complexContent
|
||||
}"
|
||||
>
|
||||
<div class="c-so-view__frame-controls__btns">
|
||||
<button
|
||||
v-for="(item, index) in statusBarItems"
|
||||
:key="index"
|
||||
class="c-icon-button"
|
||||
:class="item.cssClass"
|
||||
:title="item.name"
|
||||
@click="item.callBack"
|
||||
>
|
||||
<span class="c-icon-button__label">{{ item.name }}</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="c-icon-button icon-items-expand"
|
||||
title="View Large"
|
||||
@click="expand"
|
||||
>
|
||||
<span class="c-icon-button__label">View Large</span>
|
||||
</button>
|
||||
|
||||
</div>
|
||||
<button
|
||||
class="c-icon-button icon-3-dots c-so-view__frame-controls__more"
|
||||
title="View menu items"
|
||||
@click.prevent.stop="showMenuItems($event)"
|
||||
></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="c-so-view__local-controls c-so-view__view-large h-local-controls c-local-controls--show-on-hover">
|
||||
<button
|
||||
class="c-button icon-expand"
|
||||
title="View Large"
|
||||
@click="expand"
|
||||
></button>
|
||||
</div>
|
||||
<div class="is-missing__indicator"
|
||||
title="This item is missing"
|
||||
></div>
|
||||
|
||||
<object-view
|
||||
ref="objectView"
|
||||
class="c-so-view__object-view"
|
||||
@ -68,13 +93,13 @@
|
||||
:object-path="objectPath"
|
||||
:layout-font-size="layoutFontSize"
|
||||
:layout-font="layoutFont"
|
||||
@change-action-collection="setActionCollection"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ObjectView from './ObjectView.vue';
|
||||
import ContextMenuDropDown from './contextMenuDropDown.vue';
|
||||
import PreviewHeader from '@/ui/preview/preview-header.vue';
|
||||
import Vue from 'vue';
|
||||
|
||||
@ -89,8 +114,7 @@ const SIMPLE_CONTENT_TYPES = [
|
||||
export default {
|
||||
inject: ['openmct'],
|
||||
components: {
|
||||
ObjectView,
|
||||
ContextMenuDropDown
|
||||
ObjectView
|
||||
},
|
||||
props: {
|
||||
domainObject: {
|
||||
@ -117,22 +141,32 @@ export default {
|
||||
},
|
||||
data() {
|
||||
let objectType = this.openmct.types.get(this.domainObject.type);
|
||||
|
||||
let cssClass = objectType && objectType.definition ? objectType.definition.cssClass : 'icon-object-unknown';
|
||||
|
||||
let complexContent = !SIMPLE_CONTENT_TYPES.includes(this.domainObject.type);
|
||||
|
||||
return {
|
||||
cssClass,
|
||||
complexContent
|
||||
complexContent,
|
||||
statusBarItems: [],
|
||||
status: ''
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
classList() {
|
||||
const classList = this.domainObject.classList;
|
||||
if (!classList || !classList.length) {
|
||||
return '';
|
||||
}
|
||||
statusClass() {
|
||||
return (this.status) ? `is-status--${this.status}` : '';
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.status = this.openmct.status.get(this.domainObject.identifier);
|
||||
this.removeStatusListener = this.openmct.status.observe(this.domainObject.identifier, this.setStatus);
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.removeStatusListener();
|
||||
|
||||
return classList.join(' ');
|
||||
if (this.actionCollection) {
|
||||
this.unlistenToActionCollection();
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
@ -162,6 +196,7 @@ export default {
|
||||
},
|
||||
getPreviewHeader() {
|
||||
const domainObject = this.objectPath[0];
|
||||
const actionCollection = this.actionCollection;
|
||||
const preview = new Vue({
|
||||
components: {
|
||||
PreviewHeader
|
||||
@ -172,16 +207,41 @@ export default {
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
domainObject
|
||||
domainObject,
|
||||
actionCollection
|
||||
};
|
||||
},
|
||||
template: '<PreviewHeader :domainObject="domainObject" :hideViewSwitcher="true" :showNotebookMenuSwitcher="true"></PreviewHeader>'
|
||||
template: '<PreviewHeader :actionCollection="actionCollection" :domainObject="domainObject" :hideViewSwitcher="true" :showNotebookMenuSwitcher="true"></PreviewHeader>'
|
||||
});
|
||||
|
||||
return preview.$mount().$el;
|
||||
},
|
||||
getSelectionContext() {
|
||||
return this.$refs.objectView.getSelectionContext();
|
||||
},
|
||||
setActionCollection(actionCollection) {
|
||||
if (this.actionCollection) {
|
||||
this.unlistenToActionCollection();
|
||||
}
|
||||
|
||||
this.actionCollection = actionCollection;
|
||||
this.actionCollection.on('update', this.updateActionItems);
|
||||
this.updateActionItems(this.actionCollection.applicableActions);
|
||||
},
|
||||
unlistenToActionCollection() {
|
||||
this.actionCollection.off('update', this.updateActionItems);
|
||||
delete this.actionCollection;
|
||||
},
|
||||
updateActionItems(actionItems) {
|
||||
this.statusBarItems = this.actionCollection.getStatusBarActions();
|
||||
this.menuActionItems = this.actionCollection.getVisibleActions();
|
||||
},
|
||||
showMenuItems(event) {
|
||||
let sortedActions = this.openmct.actions._groupAndSortActions(this.menuActionItems);
|
||||
this.openmct.menus.showMenu(event.x, event.y, sortedActions);
|
||||
},
|
||||
setStatus(status) {
|
||||
this.status = status;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -1,10 +1,7 @@
|
||||
<template>
|
||||
<a
|
||||
class="c-tree__item__label c-object-label"
|
||||
:class="{
|
||||
classList,
|
||||
'is-missing': observedObject.status === 'missing'
|
||||
}"
|
||||
:class="[statusClass]"
|
||||
draggable="true"
|
||||
:href="objectLink"
|
||||
@dragstart="dragStart"
|
||||
@ -14,13 +11,11 @@
|
||||
class="c-tree__item__type-icon c-object-label__type-icon"
|
||||
:class="typeClass"
|
||||
>
|
||||
<span class="is-missing__indicator"
|
||||
title="This item is missing"
|
||||
<span class="is-status__indicator"
|
||||
:title="`This item is ${status}`"
|
||||
></span>
|
||||
</div>
|
||||
<div class="c-tree__item__name c-object-label__name"
|
||||
:class="classList"
|
||||
>
|
||||
<div class="c-tree__item__name c-object-label__name">
|
||||
{{ observedObject.name }}
|
||||
</div>
|
||||
</a>
|
||||
@ -51,18 +46,11 @@ export default {
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
observedObject: this.domainObject
|
||||
observedObject: this.domainObject,
|
||||
status: ''
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
classList() {
|
||||
const classList = this.observedObject.classList;
|
||||
if (!classList || !classList.length) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return classList.join(' ');
|
||||
},
|
||||
typeClass() {
|
||||
let type = this.openmct.types.get(this.observedObject.type);
|
||||
if (!type) {
|
||||
@ -70,6 +58,9 @@ export default {
|
||||
}
|
||||
|
||||
return type.definition.cssClass;
|
||||
},
|
||||
statusClass() {
|
||||
return (this.status) ? `is-status--${this.status}` : '';
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
@ -80,8 +71,13 @@ export default {
|
||||
this.$once('hook:destroyed', removeListener);
|
||||
}
|
||||
|
||||
this.removeStatusListener = this.openmct.status.observe(this.observedObject.identifier, this.setStatus);
|
||||
this.status = this.openmct.status.get(this.observedObject.identifier);
|
||||
this.previewAction = new PreviewAction(this.openmct);
|
||||
},
|
||||
destroyed() {
|
||||
this.removeStatusListener();
|
||||
},
|
||||
methods: {
|
||||
navigateOrPreview(event) {
|
||||
if (this.openmct.editor.isEditing()) {
|
||||
@ -112,6 +108,9 @@ export default {
|
||||
// (eg. notabook.)
|
||||
event.dataTransfer.setData("openmct/domain-object-path", serializedPath);
|
||||
event.dataTransfer.setData(`openmct/domain-object/${keyString}`, this.domainObject);
|
||||
},
|
||||
setStatus(status) {
|
||||
this.status = status;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -74,6 +74,11 @@ export default {
|
||||
this.styleRuleManager.destroy();
|
||||
delete this.styleRuleManager;
|
||||
}
|
||||
|
||||
if (this.actionCollection) {
|
||||
this.actionCollection.destroy();
|
||||
delete this.actionCollection;
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.debounceUpdateView = _.debounce(this.updateView, 10);
|
||||
@ -149,6 +154,7 @@ export default {
|
||||
|
||||
let keys = Object.keys(styleObj);
|
||||
let elemToStyle = this.getStyleReceiver();
|
||||
|
||||
keys.forEach(key => {
|
||||
if (elemToStyle) {
|
||||
if ((typeof styleObj[key] === 'string') && (styleObj[key].indexOf('__no_value') > -1)) {
|
||||
@ -207,6 +213,7 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
this.getActionCollection();
|
||||
this.currentView.show(this.viewContainer, this.openmct.editor.isEditing());
|
||||
|
||||
if (immediatelySelect) {
|
||||
@ -216,6 +223,14 @@ export default {
|
||||
|
||||
this.openmct.objectViews.on('clearData', this.clearData);
|
||||
},
|
||||
getActionCollection() {
|
||||
if (this.actionCollection) {
|
||||
this.actionCollection.destroy();
|
||||
}
|
||||
|
||||
this.actionCollection = this.openmct.actions._get(this.currentObjectPath || this.objectPath, this.currentView);
|
||||
this.$emit('change-action-collection', this.actionCollection);
|
||||
},
|
||||
show(object, viewKey, immediatelySelect, currentObjectPath) {
|
||||
this.updateStyle();
|
||||
|
||||
|
@ -2,20 +2,21 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
&.is-missing {
|
||||
border: $borderMissing;
|
||||
}
|
||||
|
||||
/*************************** HEADER */
|
||||
&__header {
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
font-size: 1.05em;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: $interiorMarginSm;
|
||||
padding: 1px 2px;
|
||||
padding: 3px;
|
||||
|
||||
.c-object-label {
|
||||
font-size: 1.05em;
|
||||
&__type-icon {
|
||||
opacity: $objectLabelTypeIconOpacity;
|
||||
}
|
||||
|
||||
&__name {
|
||||
filter: $objectLabelNameFilter;
|
||||
}
|
||||
@ -31,45 +32,118 @@
|
||||
}
|
||||
}
|
||||
|
||||
&--no-frame {
|
||||
> .c-so-view__header {
|
||||
display: none;
|
||||
/*************************** FRAME CONTROLS */
|
||||
&__frame-controls {
|
||||
display: flex;
|
||||
|
||||
&__btns,
|
||||
&__more {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
&.is-missing {
|
||||
@include isMissing($absPos: true);
|
||||
.is-in-small-container &,
|
||||
.c-fl-frame & {
|
||||
[class*="__label"] {
|
||||
// button labels
|
||||
display: none;
|
||||
|
||||
.is-missing__indicator {
|
||||
top: $interiorMargin;
|
||||
left: $interiorMargin;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__local-controls {
|
||||
// View Large button
|
||||
box-shadow: $colorLocalControlOvrBg 0 0 0 2px;
|
||||
position: absolute;
|
||||
top: $interiorMargin; right: $interiorMargin;
|
||||
z-index: 10;
|
||||
/*************************** HIDDEN FRAME */
|
||||
&--no-frame {
|
||||
> .c-so-view__header {
|
||||
visibility: hidden;
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
top: 0; right: 0; bottom: auto; left: 0;
|
||||
z-index: 2;
|
||||
|
||||
.c-object-label {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.c-so-view__frame-controls {
|
||||
background: $frameControlsColorBg;
|
||||
border-radius: $controlCr;
|
||||
box-shadow: $frameControlsShdw;
|
||||
padding: 1px;
|
||||
pointer-events: all;
|
||||
|
||||
.c-icon-button {
|
||||
color: $frameControlsColorFg;
|
||||
|
||||
&:hover {
|
||||
background: rgba($frameControlsColorFg, 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
&__btns {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
[class*="__btns"] {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
[class*="__label"] {
|
||||
// button labels
|
||||
display: none;
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
&.c-so-view--layout {
|
||||
// For sub-layouts with hidden frames, completely hide the header to avoid overlapping buttons
|
||||
> .c-so-view__header {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* HOVERS */
|
||||
&:hover {
|
||||
> .c-so-view__header {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
|
||||
&[class*='is-status'] {
|
||||
border: $borderMissing;
|
||||
}
|
||||
}
|
||||
|
||||
/*************************** OBJECT VIEW */
|
||||
&__object-view {
|
||||
flex: 1 1 auto;
|
||||
height: 0; // Chrome 73 overflow bug fix
|
||||
overflow: auto;
|
||||
|
||||
.u-fills-container {
|
||||
// Expand component types that fill a container
|
||||
@include abs();
|
||||
}
|
||||
}
|
||||
|
||||
.c-click-icon,
|
||||
.c-button {
|
||||
.c-button,
|
||||
.c-icon-button {
|
||||
// Shrink buttons a bit when they appear in a frame
|
||||
border-radius: $smallCr !important;
|
||||
font-size: 0.9em;
|
||||
padding: 3px 5px;
|
||||
}
|
||||
|
||||
&__view-large {
|
||||
display: none;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
&.has-complex-content {
|
||||
> .c-so-view__view-large { display: block; }
|
||||
}
|
||||
|
||||
&.is-status--missing {
|
||||
border: $borderMissing;
|
||||
}
|
||||
|
||||
&__object-view {
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
@ -86,4 +160,5 @@
|
||||
// This element is the recipient for object styling; cannot be display: contents
|
||||
flex: 1 1 auto;
|
||||
overflow: hidden;
|
||||
display: contents;
|
||||
}
|
||||
|
@ -22,18 +22,20 @@
|
||||
opacity: $objectLabelTypeIconOpacity;
|
||||
}
|
||||
|
||||
&.is-missing {
|
||||
@include isMissing($absPos: true);
|
||||
.is-status__indicator {
|
||||
position: absolute;
|
||||
right: -3px;
|
||||
top: -3px;
|
||||
transform: scale(0.5);
|
||||
}
|
||||
|
||||
[class*='__type-icon']:before,
|
||||
[class*='__type-icon']:after{
|
||||
opacity: $opacityMissing;
|
||||
}
|
||||
|
||||
.is-missing__indicator {
|
||||
right: -3px;
|
||||
top: -3px;
|
||||
transform: scale(0.7);
|
||||
&[class*='is-status--missing'],
|
||||
&[class*='is-status--suspect'] {
|
||||
[class*='__type-icon'] {
|
||||
&:before,
|
||||
&:after {
|
||||
opacity: $opacityMissing;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,13 +2,13 @@
|
||||
<div class="c-inspector__header">
|
||||
<div v-if="!multiSelect"
|
||||
class="c-inspector__selected c-object-label"
|
||||
:class="{'is-missing': isMissing }"
|
||||
:class="[statusClass]"
|
||||
>
|
||||
<div class="c-object-label__type-icon"
|
||||
:class="typeCssClass"
|
||||
>
|
||||
<span class="is-missing__indicator"
|
||||
title="This item is missing"
|
||||
<span class="is-status__indicator"
|
||||
:title="`This item is ${status}`"
|
||||
></span>
|
||||
</div>
|
||||
<span v-if="!singleSelectNonObject"
|
||||
@ -37,8 +37,10 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
domainObject: {},
|
||||
keyString: undefined,
|
||||
multiSelect: false,
|
||||
itemsSelected: 0
|
||||
itemsSelected: 0,
|
||||
status: undefined
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@ -58,9 +60,8 @@ export default {
|
||||
singleSelectNonObject() {
|
||||
return !this.item.identifier && !this.multiSelect;
|
||||
},
|
||||
isMissing() {
|
||||
// safe check this.domainObject since for layout objects this.domainOjbect is undefined
|
||||
return this.domainObject && this.domainObject.status === 'missing';
|
||||
statusClass() {
|
||||
return this.status ? `is-status--${this.status}` : '';
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
@ -69,25 +70,45 @@ export default {
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.openmct.selection.off('change', this.updateSelection);
|
||||
|
||||
if (this.statusUnsubscribe) {
|
||||
this.statusUnsubscribe();
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
updateSelection(selection) {
|
||||
if (this.statusUnsubscribe) {
|
||||
this.statusUnsubscribe();
|
||||
this.statusUnsubscribe = undefined;
|
||||
}
|
||||
|
||||
if (selection.length === 0 || selection[0].length === 0) {
|
||||
this.domainObject = {};
|
||||
this.resetDomainObject();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (selection.length > 1) {
|
||||
this.multiSelect = true;
|
||||
this.domainObject = {};
|
||||
this.itemsSelected = selection.length;
|
||||
this.resetDomainObject();
|
||||
|
||||
return;
|
||||
} else {
|
||||
this.multiSelect = false;
|
||||
this.domainObject = selection[0][0].context.item;
|
||||
this.keyString = this.openmct.objects.makeKeyString(this.domainObject.identifier);
|
||||
this.status = this.openmct.status.get(this.keyString);
|
||||
this.statusUnsubscribe = this.openmct.status.observe(this.keyString, this.updateStatus);
|
||||
}
|
||||
},
|
||||
resetDomainObject() {
|
||||
this.domainObject = {};
|
||||
this.status = undefined;
|
||||
this.keyString = undefined;
|
||||
},
|
||||
updateStatus(status) {
|
||||
this.status = status;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -29,6 +29,10 @@
|
||||
filter: $objectLabelNameFilter;
|
||||
}
|
||||
|
||||
.c-object-label__type-icon {
|
||||
opacity: $objectLabelTypeIconOpacity;
|
||||
}
|
||||
|
||||
&--non-domain-object .c-object-label__name {
|
||||
font-style: italic;
|
||||
}
|
||||
|
@ -9,13 +9,13 @@
|
||||
></button>
|
||||
<div
|
||||
class="l-browse-bar__object-name--w c-object-label"
|
||||
:class="[classList, { 'is-missing': domainObject.status === 'missing' }]"
|
||||
:class="[statusClass]"
|
||||
>
|
||||
<div class="c-object-label__type-icon"
|
||||
:class="type.cssClass"
|
||||
>
|
||||
<span class="is-missing__indicator"
|
||||
title="This item is missing"
|
||||
<span class="is-status__indicator"
|
||||
:title="`This item is ${status}`"
|
||||
></span>
|
||||
</div>
|
||||
<span
|
||||
@ -28,10 +28,6 @@
|
||||
{{ domainObject.name }}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="l-browse-bar__context-actions c-disclosure-button"
|
||||
@click.prevent.stop="showContextMenu"
|
||||
></div>
|
||||
</div>
|
||||
|
||||
<div class="l-browse-bar__end">
|
||||
@ -39,7 +35,6 @@
|
||||
v-if="!isEditing"
|
||||
:current-view="currentView"
|
||||
:views="views"
|
||||
@setView="setView"
|
||||
/>
|
||||
<!-- Action buttons -->
|
||||
<NotebookMenuSwitcher v-if="notebookEnabled"
|
||||
@ -49,15 +44,24 @@
|
||||
/>
|
||||
<div class="l-browse-bar__actions">
|
||||
<button
|
||||
v-if="isViewEditable && !isEditing"
|
||||
:title="lockedOrUnlocked"
|
||||
v-for="(item, index) in statusBarItems"
|
||||
:key="index"
|
||||
class="c-button"
|
||||
:class="item.cssClass"
|
||||
@click="item.callBack"
|
||||
>
|
||||
</button>
|
||||
|
||||
<button
|
||||
v-if="isViewEditable & !isEditing"
|
||||
:title="lockedOrUnlockedTitle"
|
||||
:class="{
|
||||
'icon-lock c-button--caution': domainObject.locked,
|
||||
'icon-unlocked': !domainObject.locked
|
||||
'c-button icon-lock': domainObject.locked,
|
||||
'c-icon-button icon-unlocked': !domainObject.locked
|
||||
}"
|
||||
@click="toggleLock(!domainObject.locked)"
|
||||
></button>
|
||||
|
||||
<button
|
||||
v-if="isViewEditable && !isEditing && !domainObject.locked"
|
||||
class="l-browse-bar__actions__edit c-button c-button--major icon-pencil"
|
||||
@ -103,6 +107,11 @@
|
||||
title="Cancel Editing"
|
||||
@click="promptUserandCancelEditing()"
|
||||
></button>
|
||||
<button
|
||||
class="l-browse-bar__actions c-icon-button icon-3-dots"
|
||||
title="More options"
|
||||
@click.prevent.stop="showMenuItems($event)"
|
||||
></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -120,6 +129,14 @@ export default {
|
||||
NotebookMenuSwitcher,
|
||||
ViewSwitcher
|
||||
},
|
||||
props: {
|
||||
actionCollection: {
|
||||
type: Object,
|
||||
default: () => {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
},
|
||||
data: function () {
|
||||
return {
|
||||
notebookTypes: [],
|
||||
@ -128,17 +145,14 @@ export default {
|
||||
domainObject: PLACEHOLDER_OBJECT,
|
||||
viewKey: undefined,
|
||||
isEditing: this.openmct.editor.isEditing(),
|
||||
notebookEnabled: this.openmct.types.get('notebook')
|
||||
notebookEnabled: this.openmct.types.get('notebook'),
|
||||
statusBarItems: [],
|
||||
status: ''
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
classList() {
|
||||
const classList = this.domainObject.classList;
|
||||
if (!classList || !classList.length) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return classList.join(' ');
|
||||
statusClass() {
|
||||
return (this.status) ? `is-status--${this.status}` : '';
|
||||
},
|
||||
currentView() {
|
||||
return this.views.filter(v => v.key === this.viewKey)[0] || {};
|
||||
@ -152,7 +166,10 @@ export default {
|
||||
return {
|
||||
key: p.key,
|
||||
cssClass: p.cssClass,
|
||||
name: p.name
|
||||
name: p.name,
|
||||
callBack: () => {
|
||||
return this.setView({key: p.key});
|
||||
}
|
||||
};
|
||||
});
|
||||
},
|
||||
@ -184,7 +201,7 @@ export default {
|
||||
|
||||
return false;
|
||||
},
|
||||
lockedOrUnlocked() {
|
||||
lockedOrUnlockedTitle() {
|
||||
if (this.domainObject.locked) {
|
||||
return 'Locked for editing - click to unlock.';
|
||||
} else {
|
||||
@ -201,6 +218,22 @@ export default {
|
||||
this.mutationObserver = this.openmct.objects.observe(this.domainObject, '*', (domainObject) => {
|
||||
this.domainObject = domainObject;
|
||||
});
|
||||
|
||||
if (this.removeStatusListener) {
|
||||
this.removeStatusListener();
|
||||
}
|
||||
|
||||
this.status = this.openmct.status.get(this.domainObject.identifier, this.setStatus);
|
||||
this.removeStatusListener = this.openmct.status.observe(this.domainObject.identifier, this.setStatus);
|
||||
},
|
||||
actionCollection(actionCollection) {
|
||||
if (this.actionCollection) {
|
||||
this.unlistenToActionCollection();
|
||||
}
|
||||
|
||||
this.actionCollection = actionCollection;
|
||||
this.actionCollection.on('update', this.updateActionItems);
|
||||
this.updateActionItems(this.actionCollection.getActionsObject());
|
||||
}
|
||||
},
|
||||
mounted: function () {
|
||||
@ -216,6 +249,14 @@ export default {
|
||||
this.mutationObserver();
|
||||
}
|
||||
|
||||
if (this.actionCollection) {
|
||||
this.unlistenToActionCollection();
|
||||
}
|
||||
|
||||
if (this.removeStatusListener) {
|
||||
this.removeStatusListener();
|
||||
}
|
||||
|
||||
document.removeEventListener('click', this.closeViewAndSaveMenu);
|
||||
window.removeEventListener('click', this.promptUserbeforeNavigatingAway);
|
||||
},
|
||||
@ -297,14 +338,26 @@ export default {
|
||||
this.openmct.editor.edit();
|
||||
});
|
||||
},
|
||||
showContextMenu(event) {
|
||||
this.openmct.contextMenu._showContextMenuForObjectPath(this.openmct.router.path, event.clientX, event.clientY);
|
||||
},
|
||||
goToParent() {
|
||||
window.location.hash = this.parentUrl;
|
||||
},
|
||||
updateActionItems(actionItems) {
|
||||
this.statusBarItems = this.actionCollection.getStatusBarActions();
|
||||
this.menuActionItems = this.actionCollection.getVisibleActions();
|
||||
},
|
||||
showMenuItems(event) {
|
||||
let sortedActions = this.openmct.actions._groupAndSortActions(this.menuActionItems);
|
||||
this.openmct.menus.showMenu(event.x, event.y, sortedActions);
|
||||
},
|
||||
unlistenToActionCollection() {
|
||||
this.actionCollection.off('update', this.updateActionItems);
|
||||
delete this.actionCollection;
|
||||
},
|
||||
toggleLock(flag) {
|
||||
this.openmct.objects.mutate(this.domainObject, 'locked', flag);
|
||||
},
|
||||
setStatus(status) {
|
||||
this.status = status;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -70,6 +70,7 @@
|
||||
<browse-bar
|
||||
ref="browseBar"
|
||||
class="l-shell__main-view-browse-bar"
|
||||
:action-collection="actionCollection"
|
||||
@sync-tree-navigation="handleSyncTreeNavigation"
|
||||
/>
|
||||
<toolbar
|
||||
@ -79,8 +80,9 @@
|
||||
<object-view
|
||||
ref="browseObject"
|
||||
class="l-shell__main-container"
|
||||
:show-edit-view="true"
|
||||
data-selectable
|
||||
:show-edit-view="true"
|
||||
@change-action-collection="setActionCollection"
|
||||
/>
|
||||
<component
|
||||
:is="conductorComponent"
|
||||
@ -144,8 +146,9 @@ export default {
|
||||
conductorComponent: undefined,
|
||||
isEditing: false,
|
||||
hasToolbar: false,
|
||||
headExpanded,
|
||||
triggerSync: false
|
||||
actionCollection: undefined,
|
||||
triggerSync: false,
|
||||
headExpanded
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@ -220,6 +223,9 @@ export default {
|
||||
|
||||
this.hasToolbar = structure.length > 0;
|
||||
},
|
||||
setActionCollection(actionCollection) {
|
||||
this.actionCollection = actionCollection;
|
||||
},
|
||||
handleSyncTreeNavigation() {
|
||||
this.triggerSync = !this.triggerSync;
|
||||
}
|
||||
|
@ -4,36 +4,21 @@
|
||||
class="l-browse-bar__view-switcher c-ctrl-wrapper c-ctrl-wrapper--menus-left"
|
||||
>
|
||||
<button
|
||||
class="c-button--menu"
|
||||
class="c-icon-button c-button--menu"
|
||||
:class="currentView.cssClass"
|
||||
title="Change the current view"
|
||||
@click.stop="toggleViewMenu"
|
||||
@click.prevent.stop="showMenu"
|
||||
>
|
||||
<span class="c-button__label">
|
||||
<span class="c-icon-button__label">
|
||||
{{ currentView.name }}
|
||||
</span>
|
||||
</button>
|
||||
<div
|
||||
v-show="showViewMenu"
|
||||
class="c-menu"
|
||||
>
|
||||
<ul>
|
||||
<li
|
||||
v-for="(view, index) in views"
|
||||
:key="index"
|
||||
:class="view.cssClass"
|
||||
:title="view.name"
|
||||
@click="setView(view)"
|
||||
>
|
||||
{{ view.name }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
inject: ['openmct'],
|
||||
props: {
|
||||
currentView: {
|
||||
type: Object,
|
||||
@ -44,26 +29,16 @@ export default {
|
||||
required: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showViewMenu: false
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
document.addEventListener('click', this.hideViewMenu);
|
||||
},
|
||||
destroyed() {
|
||||
document.removeEventListener('click', this.hideViewMenu);
|
||||
},
|
||||
methods: {
|
||||
setView(view) {
|
||||
this.$emit('setView', view);
|
||||
},
|
||||
toggleViewMenu() {
|
||||
this.showViewMenu = !this.showViewMenu;
|
||||
},
|
||||
hideViewMenu() {
|
||||
this.showViewMenu = false;
|
||||
showMenu() {
|
||||
const elementBoundingClientRect = this.$el.getBoundingClientRect();
|
||||
const x = elementBoundingClientRect.x;
|
||||
const y = elementBoundingClientRect.y + elementBoundingClientRect.height;
|
||||
|
||||
this.openmct.menus.showMenu(x, y, this.views);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -333,7 +333,7 @@
|
||||
}
|
||||
|
||||
> * + * {
|
||||
margin-left: $interiorMargin;
|
||||
margin-left: $interiorMarginSm;
|
||||
}
|
||||
}
|
||||
|
||||
@ -371,12 +371,16 @@
|
||||
filter: $objectLabelNameFilter;
|
||||
}
|
||||
|
||||
.c-object-label__type-icon {
|
||||
opacity: $objectLabelTypeIconOpacity;
|
||||
}
|
||||
|
||||
&__object-name--w {
|
||||
@include headerFont(1.5em);
|
||||
margin-left: $interiorMarginLg;
|
||||
min-width: 0;
|
||||
|
||||
.is-missing__indicator {
|
||||
.is-status__indicator {
|
||||
right: -5px !important;
|
||||
top: -4px !important;
|
||||
}
|
||||
|
@ -30,7 +30,12 @@ export default {
|
||||
showContextMenu(event) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
this.openmct.contextMenu._showContextMenuForObjectPath(this.objectPath, event.clientX, event.clientY);
|
||||
|
||||
let actionsCollection = this.openmct.actions.get(this.objectPath);
|
||||
let actions = actionsCollection.getVisibleActions();
|
||||
let sortedActions = this.openmct.actions._groupAndSortActions(actions);
|
||||
|
||||
this.openmct.menus.showMenu(event.clientX, event.clientY, sortedActions);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -23,9 +23,9 @@
|
||||
<div class="l-preview-window">
|
||||
<PreviewHeader
|
||||
:current-view="currentView"
|
||||
:action-collection="actionCollection"
|
||||
:domain-object="domainObject"
|
||||
:views="views"
|
||||
@setView="setView"
|
||||
/>
|
||||
<div class="l-preview-window__object-view">
|
||||
<div ref="objectView"></div>
|
||||
@ -51,26 +51,23 @@ export default {
|
||||
|
||||
return {
|
||||
domainObject: domainObject,
|
||||
viewKey: undefined
|
||||
viewKey: undefined,
|
||||
views: [],
|
||||
currentView: {},
|
||||
actionCollection: undefined
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
views() {
|
||||
return this
|
||||
.openmct
|
||||
.objectViews
|
||||
.get(this.domainObject);
|
||||
},
|
||||
currentView() {
|
||||
return this.views.filter(v => v.key === this.viewKey)[0] || {};
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
let view = this.openmct.objectViews.get(this.domainObject)[0];
|
||||
this.setView(view);
|
||||
this.views = this.openmct.objectViews.get(this.domainObject).map((view) => {
|
||||
view.callBack = () => {
|
||||
return this.setView(view);
|
||||
};
|
||||
|
||||
return view;
|
||||
});
|
||||
this.setView(this.views[0]);
|
||||
},
|
||||
destroyed() {
|
||||
this.view.destroy();
|
||||
beforeDestroy() {
|
||||
if (this.stopListeningStyles) {
|
||||
this.stopListeningStyles();
|
||||
}
|
||||
@ -79,6 +76,13 @@ export default {
|
||||
this.styleRuleManager.destroy();
|
||||
delete this.styleRuleManager;
|
||||
}
|
||||
|
||||
if (this.actionCollection) {
|
||||
this.actionCollection.destroy();
|
||||
}
|
||||
},
|
||||
destroyed() {
|
||||
this.view.destroy();
|
||||
},
|
||||
methods: {
|
||||
clear() {
|
||||
@ -91,17 +95,29 @@ export default {
|
||||
delete this.viewContainer;
|
||||
},
|
||||
setView(view) {
|
||||
this.clear();
|
||||
if (this.viewKey === view.key) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.clear();
|
||||
this.viewKey = view.key;
|
||||
this.currentView = view;
|
||||
this.viewContainer = document.createElement('div');
|
||||
this.viewContainer.classList.add('l-angular-ov-wrapper');
|
||||
this.$refs.objectView.append(this.viewContainer);
|
||||
|
||||
this.view = this.currentView.view(this.domainObject, this.objectPath);
|
||||
|
||||
this.getActionsCollection();
|
||||
this.view.show(this.viewContainer, false);
|
||||
this.initObjectStyles();
|
||||
},
|
||||
getActionsCollection() {
|
||||
if (this.actionCollection) {
|
||||
this.actionCollection.destroy();
|
||||
}
|
||||
|
||||
this.actionCollection = this.openmct.actions._get(this.objectPath, this.view);
|
||||
},
|
||||
initObjectStyles() {
|
||||
if (!this.styleRuleManager) {
|
||||
this.styleRuleManager = new StyleRuleManager((this.domainObject.configuration && this.domainObject.configuration.objectStyles), this.openmct, this.updateStyle.bind(this));
|
||||
@ -124,8 +140,9 @@ export default {
|
||||
}
|
||||
|
||||
let keys = Object.keys(styleObj);
|
||||
let firstChild = this.$refs.objectView.querySelector(':first-child');
|
||||
|
||||
keys.forEach(key => {
|
||||
let firstChild = this.$refs.objectView.querySelector(':first-child');
|
||||
if (firstChild) {
|
||||
if ((typeof styleObj[key] === 'string') && (styleObj[key].indexOf('__no_value') > -1)) {
|
||||
if (firstChild.style[key]) {
|
||||
|
@ -27,10 +27,12 @@ export default class PreviewAction {
|
||||
/**
|
||||
* Metadata
|
||||
*/
|
||||
this.name = 'Preview';
|
||||
this.name = 'View';
|
||||
this.key = 'preview';
|
||||
this.description = 'Preview in large dialog';
|
||||
this.cssClass = 'icon-eye-open';
|
||||
this.description = 'View in large dialog';
|
||||
this.cssClass = 'icon-items-expand';
|
||||
this.group = 'windowing';
|
||||
this.priority = 1;
|
||||
|
||||
/**
|
||||
* Dependencies
|
||||
@ -81,8 +83,7 @@ export default class PreviewAction {
|
||||
let targetObject = objectPath[0];
|
||||
let navigatedObject = this._openmct.router.path[0];
|
||||
|
||||
return targetObject.identifier.namespace === navigatedObject.identifier.namespace
|
||||
&& targetObject.identifier.key === navigatedObject.identifier.key;
|
||||
return this._openmct.objects.areIdsEqual(targetObject.identifier, navigatedObject.identifier);
|
||||
}
|
||||
_preventPreview(objectPath) {
|
||||
const noPreviewTypes = ['folder'];
|
||||
|
@ -32,4 +32,14 @@ export default class ViewHistoricalDataAction extends PreviewAction {
|
||||
this.cssClass = 'icon-eye-open';
|
||||
this.hideInDefaultMenu = true;
|
||||
}
|
||||
|
||||
appliesTo(objectPath, view = {}) {
|
||||
let viewContext = view.getViewContext && view.getViewContext();
|
||||
|
||||
if (objectPath.length && viewContext && viewContext.viewHistoricalData) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -24,7 +24,7 @@ import ViewHistoricalDataAction from './ViewHistoricalDataAction';
|
||||
|
||||
export default function () {
|
||||
return function (openmct) {
|
||||
openmct.contextMenu.registerAction(new PreviewAction(openmct));
|
||||
openmct.contextMenu.registerAction(new ViewHistoricalDataAction(openmct));
|
||||
openmct.actions.register(new PreviewAction(openmct));
|
||||
openmct.actions.register(new ViewHistoricalDataAction(openmct));
|
||||
};
|
||||
}
|
||||
|
@ -13,13 +13,25 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="l-browse-bar__end">
|
||||
<view-switcher
|
||||
:v-if="!hideViewSwitcher"
|
||||
:views="views"
|
||||
:current-view="currentView"
|
||||
/>
|
||||
<div class="l-browse-bar__actions">
|
||||
<view-switcher
|
||||
:v-if="!hideViewSwitcher"
|
||||
:views="views"
|
||||
:current-view="currentView"
|
||||
@setView="setView"
|
||||
/>
|
||||
<button
|
||||
v-for="(item, index) in statusBarItems"
|
||||
:key="index"
|
||||
class="c-button"
|
||||
:class="item.cssClass"
|
||||
@click="item.callBack"
|
||||
>
|
||||
</button>
|
||||
<button
|
||||
class="l-browse-bar__actions c-icon-button icon-3-dots"
|
||||
title="More options"
|
||||
@click.prevent.stop="showMenuItems($event)"
|
||||
></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -27,6 +39,11 @@
|
||||
|
||||
<script>
|
||||
import ViewSwitcher from '../../ui/layout/ViewSwitcher.vue';
|
||||
const HIDDEN_ACTIONS = [
|
||||
'remove',
|
||||
'move',
|
||||
'preview'
|
||||
];
|
||||
|
||||
export default {
|
||||
inject: [
|
||||
@ -59,16 +76,53 @@ export default {
|
||||
default: () => {
|
||||
return [];
|
||||
}
|
||||
},
|
||||
actionCollection: {
|
||||
type: Object,
|
||||
default: () => {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
type: this.openmct.types.get(this.domainObject.type)
|
||||
type: this.openmct.types.get(this.domainObject.type),
|
||||
statusBarItems: [],
|
||||
menuActionItems: []
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
actionCollection(actionCollection) {
|
||||
if (this.actionCollection) {
|
||||
this.unlistenToActionCollection();
|
||||
}
|
||||
|
||||
this.actionCollection.on('update', this.updateActionItems);
|
||||
this.updateActionItems(this.actionCollection.getActionsObject());
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
if (this.actionCollection) {
|
||||
this.actionCollection.on('update', this.updateActionItems);
|
||||
this.updateActionItems(this.actionCollection.getActionsObject());
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
setView(view) {
|
||||
this.$emit('setView', view);
|
||||
},
|
||||
unlistenToActionCollection() {
|
||||
this.actionCollection.off('update', this.updateActionItems);
|
||||
delete this.actionCollection;
|
||||
},
|
||||
updateActionItems() {
|
||||
this.actionCollection.hide(HIDDEN_ACTIONS);
|
||||
this.statusBarItems = this.actionCollection.getStatusBarActions();
|
||||
this.menuActionItems = this.actionCollection.getVisibleActions();
|
||||
},
|
||||
showMenuItems(event) {
|
||||
let sortedActions = this.openmct.actions._groupAndSortActions(this.menuActionItems);
|
||||
this.openmct.menus.showMenu(event.x, event.y, sortedActions);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
13
src/utils/clipboard.js
Normal file
13
src/utils/clipboard.js
Normal 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();
|
Loading…
x
Reference in New Issue
Block a user