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. -->
- +
{{model.name}}
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 @@ *****************************************************************************/