diff --git a/platform/commonUI/browse/bundle.js b/platform/commonUI/browse/bundle.js
index d57c93921a..e64d259298 100644
--- a/platform/commonUI/browse/bundle.js
+++ b/platform/commonUI/browse/bundle.js
@@ -143,8 +143,8 @@ define([
"$window"
],
"group": "windowing",
- "cssClass": "icon-new-window",
- "priority": "preferred"
+ "priority": 10,
+ "cssClass": "icon-new-window"
}
],
"runs": [
diff --git a/platform/commonUI/edit/bundle.js b/platform/commonUI/edit/bundle.js
index 9dce4f3133..12d6175655 100644
--- a/platform/commonUI/edit/bundle.js
+++ b/platform/commonUI/edit/bundle.js
@@ -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"
diff --git a/platform/commonUI/general/res/templates/label.html b/platform/commonUI/general/res/templates/label.html
index 5f6f1ecad6..8ea189ec67 100644
--- a/platform/commonUI/general/res/templates/label.html
+++ b/platform/commonUI/general/res/templates/label.html
@@ -20,12 +20,12 @@
at runtime from the About dialog for additional information.
-->
diff --git a/platform/entanglement/bundle.js b/platform/entanglement/bundle.js
index d7339d4c5b..9715ba20c0 100644
--- a/platform/entanglement/bundle.js
+++ b/platform/entanglement/bundle.js
@@ -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",
diff --git a/platform/import-export/bundle.js b/platform/import-export/bundle.js
index 933cb8e1e8..6d7cd8017c 100644
--- a/platform/import-export/bundle.js
+++ b/platform/import-export/bundle.js
@@ -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",
diff --git a/src/MCT.js b/src/MCT.js
index 227d3eb0b2..e3ed1ab945 100644
--- a/src/MCT.js
+++ b/src/MCT.js
@@ -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);
diff --git a/src/adapter/actions/LegacyActionAdapter.js b/src/adapter/actions/LegacyActionAdapter.js
index 6633734037..9aa3edbb6a 100644
--- a/src/adapter/actions/LegacyActionAdapter.js
+++ b/src/adapter/actions/LegacyActionAdapter.js
@@ -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);
}
diff --git a/src/adapter/actions/LegacyContextMenuAction.js b/src/adapter/actions/LegacyContextMenuAction.js
index f1c6d4ffa1..22e72bcefb 100644
--- a/src/adapter/actions/LegacyContextMenuAction.js
+++ b/src/adapter/actions/LegacyContextMenuAction.js
@@ -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) {
diff --git a/src/api/actions/ActionCollection.js b/src/api/actions/ActionCollection.js
new file mode 100644
index 0000000000..7ba71e9457
--- /dev/null
+++ b/src/api/actions/ActionCollection.js
@@ -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;
diff --git a/src/api/actions/ActionsAPI.js b/src/api/actions/ActionsAPI.js
new file mode 100644
index 0000000000..6170f421f1
--- /dev/null
+++ b/src/api/actions/ActionsAPI.js
@@ -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;
diff --git a/src/api/actions/ActionsAPISpec.js b/src/api/actions/ActionsAPISpec.js
new file mode 100644
index 0000000000..41c517e100
--- /dev/null
+++ b/src/api/actions/ActionsAPISpec.js
@@ -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);
+ });
+ });
+});
diff --git a/src/api/api.js b/src/api/api.js
index b7b57d4cca..6d365a8e77 100644
--- a/src/api/api.js
+++ b/src/api/api.js
@@ -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
};
});
diff --git a/src/api/contextMenu/ContextMenu.vue b/src/api/contextMenu/ContextMenu.vue
deleted file mode 100644
index 727a7a4387..0000000000
--- a/src/api/contextMenu/ContextMenu.vue
+++ /dev/null
@@ -1,24 +0,0 @@
-
-
-
-
-
diff --git a/src/api/contextMenu/ContextMenuAPI.js b/src/api/contextMenu/ContextMenuAPI.js
deleted file mode 100644
index fdbf376a8e..0000000000
--- a/src/api/contextMenu/ContextMenuAPI.js
+++ /dev/null
@@ -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: ''
- });
- }
-}
-export default ContextMenuAPI;
diff --git a/src/api/menu/MenuAPI.js b/src/api/menu/MenuAPI.js
new file mode 100644
index 0000000000..c9d3375fd9
--- /dev/null
+++ b/src/api/menu/MenuAPI.js
@@ -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;
diff --git a/src/api/menu/MenuAPISpec.js b/src/api/menu/MenuAPISpec.js
new file mode 100644
index 0000000000..241a0a2795
--- /dev/null
+++ b/src/api/menu/MenuAPISpec.js
@@ -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();
+ });
+ });
+ });
+});
diff --git a/src/api/menu/components/Menu.vue b/src/api/menu/components/Menu.vue
new file mode 100644
index 0000000000..7d687cbbe0
--- /dev/null
+++ b/src/api/menu/components/Menu.vue
@@ -0,0 +1,52 @@
+
+
+
+
+
diff --git a/src/api/menu/menu.js b/src/api/menu/menu.js
new file mode 100644
index 0000000000..8a515109b7
--- /dev/null
+++ b/src/api/menu/menu.js
@@ -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: ''
+ });
+
+ 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;
diff --git a/src/api/overlays/OverlayAPI.js b/src/api/overlays/OverlayAPI.js
index d68116d012..9c2b1fb28b 100644
--- a/src/api/overlays/OverlayAPI.js
+++ b/src/api/overlays/OverlayAPI.js
@@ -22,6 +22,7 @@ class OverlayAPI {
this.dismissLastOverlay();
}
});
+
}
/**
@@ -127,6 +128,7 @@ class OverlayAPI {
return progressDialog;
}
+
}
export default OverlayAPI;
diff --git a/src/api/status/StatusAPI.js b/src/api/status/StatusAPI.js
new file mode 100644
index 0000000000..70e5b1fb4e
--- /dev/null
+++ b/src/api/status/StatusAPI.js
@@ -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);
+ };
+ }
+}
diff --git a/src/api/status/StatusAPISpec.js b/src/api/status/StatusAPISpec.js
new file mode 100644
index 0000000000..345bc4faf3
--- /dev/null
+++ b/src/api/status/StatusAPISpec.js
@@ -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);
+ });
+ });
+});
diff --git a/src/plugins/LADTable/components/LADRow.vue b/src/plugins/LADTable/components/LADRow.vue
index e36b18fdd4..dbedfbc81a 100644
--- a/src/plugins/LADTable/components/LADRow.vue
+++ b/src/plugins/LADTable/components/LADRow.vue
@@ -44,6 +44,7 @@
diff --git a/src/plugins/viewDatumAction/components/metadata-list.scss b/src/plugins/viewDatumAction/components/metadata-list.scss
new file mode 100644
index 0000000000..778d4f3ded
--- /dev/null
+++ b/src/plugins/viewDatumAction/components/metadata-list.scss
@@ -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;
+ }
+ }
+
+
+}
diff --git a/src/plugins/viewDatumAction/plugin.js b/src/plugins/viewDatumAction/plugin.js
new file mode 100644
index 0000000000..600f961262
--- /dev/null
+++ b/src/plugins/viewDatumAction/plugin.js
@@ -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));
+ };
+}
diff --git a/src/plugins/viewDatumAction/pluginSpec.js b/src/plugins/viewDatumAction/pluginSpec.js
new file mode 100644
index 0000000000..80125f531e
--- /dev/null
+++ b/src/plugins/viewDatumAction/pluginSpec.js
@@ -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();
+ });
+ });
+});
diff --git a/src/styles/_constants-espresso.scss b/src/styles/_constants-espresso.scss
index cbc315db2a..bd4250f006 100644
--- a/src/styles/_constants-espresso.scss
+++ b/src/styles/_constants-espresso.scss
@@ -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;
diff --git a/src/styles/_constants-maelstrom.scss b/src/styles/_constants-maelstrom.scss
index 415d7c5969..714cace65d 100644
--- a/src/styles/_constants-maelstrom.scss
+++ b/src/styles/_constants-maelstrom.scss
@@ -382,6 +382,11 @@ $colorItemTreeVC: $colorDisclosureCtrl;
$colorItemTreeVCHover: $colorDisclosureCtrlHov;
$shdwItemTreeIcon: none;
+// Layout frame controls
+$frameControlsColorFg: white;
+$frameControlsColorBg: $colorKey;
+$frameControlsShdw: $shdwMenu;
+
// Images
$colorThumbHoverBg: $colorItemTreeHoverBg;
diff --git a/src/styles/_constants-snow.scss b/src/styles/_constants-snow.scss
index ccaf0246be..cf53c20da0 100644
--- a/src/styles/_constants-snow.scss
+++ b/src/styles/_constants-snow.scss
@@ -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;
diff --git a/src/styles/_controls.scss b/src/styles/_controls.scss
index fb17882824..9ddacd2555 100644
--- a/src/styles/_controls.scss
+++ b/src/styles/_controls.scss
@@ -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);
}
}
diff --git a/src/styles/_legacy-plots.scss b/src/styles/_legacy-plots.scss
index 9986c5bfc5..2da35f7107 100644
--- a/src/styles/_legacy-plots.scss
+++ b/src/styles/_legacy-plots.scss
@@ -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;
}
}
diff --git a/src/styles/_mixins.scss b/src/styles/_mixins.scss
index 098f23505a..7d5f3d0ce8 100644
--- a/src/styles/_mixins.scss
+++ b/src/styles/_mixins.scss
@@ -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"] {
diff --git a/src/styles/_status.scss b/src/styles/_status.scss
index f10e811f05..670eb15f02 100644
--- a/src/styles/_status.scss
+++ b/src/styles/_status.scss
@@ -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;
+ }
+ }
+}
diff --git a/src/styles/_table.scss b/src/styles/_table.scss
index 07176625b9..ab710f6a9c 100644
--- a/src/styles/_table.scss
+++ b/src/styles/_table.scss
@@ -97,6 +97,7 @@ div.c-table {
}
}
.c-table-control-bar {
+ .c-icon-button,
.c-click-icon,
.c-button {
&__label {
diff --git a/src/styles/notebook.scss b/src/styles/notebook.scss
index 996b7403a8..743b758d02 100644
--- a/src/styles/notebook.scss
+++ b/src/styles/notebook.scss
@@ -213,7 +213,8 @@
}
}
-.is-notebook-default {
+.is-notebook-default,
+.is-status--notebook-default {
&:after {
color: $colorFilter;
content: $glyph-icon-notebook-page;
diff --git a/src/styles/vue-styles.scss b/src/styles/vue-styles.scss
index 4ffa0ea19b..4fab26511e 100644
--- a/src/styles/vue-styles.scss
+++ b/src/styles/vue-styles.scss
@@ -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";
diff --git a/src/ui/components/ObjectFrame.vue b/src/ui/components/ObjectFrame.vue
index 966a30ac03..15c9c64d6f 100644
--- a/src/ui/components/ObjectFrame.vue
+++ b/src/ui/components/ObjectFrame.vue
@@ -21,45 +21,70 @@
*****************************************************************************/
-