Context-Menu for Tables (#2424)

* add context menu capability to table rows, add view switcher to preview

* add an option to limit context menu actions to the ones requested, and modify preview action to also include a view historical data action

* extend preview action into view historical data action

* add context menu to LAD Table

* add keys to context menu actions, allow tables to conditionally attach context menu handler

* working switch y axis label

* New vertical select element for Y axis configuration in plots

- CSS for vertically rotated selects for Y axis label selection;
- New theme constants;
- Removed themedSelect theme mixins;
- New SASS svgColorFromHex function;

* use keys in lad table context menu options

* show historical view context menu on alpha-numerics

* make reviewer requested changes

* pass contextual object path from object view down etc

* made reviewer requested changes: removed options object, pass in object path instead

* remove redundant function from LADRow.vue
This commit is contained in:
Deep Tailor
2019-07-26 16:09:59 -07:00
committed by Andrew Henry
parent 768d99d928
commit 97230bb21f
35 changed files with 346 additions and 94 deletions

View File

@ -26,6 +26,7 @@ const OUTSIDE_EDIT_PATH_BLACKLIST = ["copy", "follow", "properties", "move", "li
export default class LegacyContextMenuAction { export default class LegacyContextMenuAction {
constructor(openmct, LegacyAction) { constructor(openmct, LegacyAction) {
this.openmct = openmct; this.openmct = openmct;
this.key = LegacyAction.definition.key;
this.name = LegacyAction.definition.name; this.name = LegacyAction.definition.name;
this.description = LegacyAction.definition.description; this.description = LegacyAction.definition.description;
this.cssClass = LegacyAction.definition.cssClass; this.cssClass = LegacyAction.definition.cssClass;

View File

@ -49,6 +49,9 @@ class ContextMenuAPI {
* a single sentence or short paragraph) of this kind of view * a single sentence or short paragraph) of this kind of view
* @property {string} cssClass the CSS class to apply to labels for this * @property {string} cssClass the CSS class to apply to labels for this
* view (to add icons, for instance) * 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 * @method appliesTo
@ -72,12 +75,21 @@ class ContextMenuAPI {
/** /**
* @private * @private
*/ */
_showContextMenuForObjectPath(objectPath, x, y) { _showContextMenuForObjectPath(objectPath, x, y, actionsToBeIncluded) {
let applicableActions = this._allActions.filter((action) => { let applicableActions = this._allActions.filter((action) => {
if (action.appliesTo === undefined) {
return true; 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;
} }
return action.appliesTo(objectPath);
}); });
if (this._activeContextMenu) { if (this._activeContextMenu) {

View File

@ -38,7 +38,7 @@ define([
canEdit: function (domainObject) { canEdit: function (domainObject) {
return domainObject.type === 'LadTableSet'; return domainObject.type === 'LadTableSet';
}, },
view: function (domainObject) { view: function (domainObject, isEditing, objectPath) {
let component; let component;
return { return {
@ -49,7 +49,8 @@ define([
}, },
provide: { provide: {
openmct, openmct,
domainObject domainObject,
objectPath
}, },
el: element, el: element,
template: '<lad-table-set></lad-table-set>' template: '<lad-table-set></lad-table-set>'

View File

@ -38,7 +38,7 @@ define([
canEdit: function (domainObject) { canEdit: function (domainObject) {
return domainObject.type === 'LadTable'; return domainObject.type === 'LadTable';
}, },
view: function (domainObject) { view: function (domainObject, isEditing, objectPath) {
let component; let component;
return { return {
@ -49,7 +49,8 @@ define([
}, },
provide: { provide: {
openmct, openmct,
domainObject domainObject,
objectPath
}, },
el: element, el: element,
template: '<lad-table-component></lad-table-component>' template: '<lad-table-component></lad-table-component>'

View File

@ -1,3 +1,4 @@
/***************************************************************************** /*****************************************************************************
* Open MCT, Copyright (c) 2014-2018, United States Government * Open MCT, Copyright (c) 2014-2018, United States Government
* as represented by the Administrator of the National Aeronautics and Space * as represented by the Administrator of the National Aeronautics and Space
@ -21,7 +22,7 @@
*****************************************************************************/ *****************************************************************************/
<template> <template>
<tr> <tr @contextmenu.prevent="showContextMenu">
<td>{{name}}</td> <td>{{name}}</td>
<td>{{timestamp}}</td> <td>{{timestamp}}</td>
<td :class="valueClass"> <td :class="valueClass">
@ -35,15 +36,25 @@
</style> </style>
<script> <script>
const CONTEXT_MENU_ACTIONS = [
'viewHistoricalData',
'remove'
];
export default { export default {
inject: ['openmct'], inject: ['openmct', 'objectPath'],
props: ['domainObject'], props: ['domainObject'],
data() { data() {
let currentObjectPath = this.objectPath.slice();
currentObjectPath.unshift(this.domainObject);
return { return {
name: this.domainObject.name, name: this.domainObject.name,
timestamp: '---', timestamp: '---',
value: '---', value: '---',
valueClass: '' valueClass: '',
currentObjectPath
} }
}, },
methods: { methods: {
@ -73,11 +84,15 @@ export default {
.request(this.domainObject, {strategy: 'latest'}) .request(this.domainObject, {strategy: 'latest'})
.then((array) => this.updateValues(array[array.length - 1])); .then((array) => this.updateValues(array[array.length - 1]));
},
showContextMenu(event) {
this.openmct.contextMenu._showContextMenuForObjectPath(this.currentObjectPath, event.x, event.y, CONTEXT_MENU_ACTIONS);
} }
}, },
mounted() { mounted() {
this.metadata = this.openmct.telemetry.getMetadata(this.domainObject); this.metadata = this.openmct.telemetry.getMetadata(this.domainObject);
this.formats = this.openmct.telemetry.getFormatMap(this.metadata); this.formats = this.openmct.telemetry.getFormatMap(this.metadata);
this.keyString = this.openmct.objects.makeKeyString(this.domainObject.identifier);
this.limitEvaluator = openmct this.limitEvaluator = openmct
.telemetry .telemetry

View File

@ -44,7 +44,7 @@ import lodash from 'lodash';
import LadRow from './LADRow.vue'; import LadRow from './LADRow.vue';
export default { export default {
inject: ['openmct', 'domainObject'], inject: ['openmct', 'domainObject', 'objectPath'],
components: { components: {
LadRow LadRow
}, },

View File

@ -46,13 +46,14 @@ define([
return selection.every(isTelemetryObject); return selection.every(isTelemetryObject);
}, },
view: function (selection) { view: function (domainObject, isEditing, objectPath) {
let component; let component;
return { return {
show: function (element) { show: function (element) {
component = new Vue({ component = new Vue({
provide: { provide: {
openmct openmct,
objectPath
}, },
components: { components: {
AlphanumericFormatView: AlphanumericFormatView.default AlphanumericFormatView: AlphanumericFormatView.default

View File

@ -202,7 +202,7 @@
return selectionPath && selectionPath.length > 1 && !singleSelectedLine; return selectionPath && selectionPath.length > 1 && !singleSelectedLine;
} }
}, },
inject: ['openmct', 'options'], inject: ['openmct', 'options', 'objectPath'],
props: ['domainObject'], props: ['domainObject'],
components: components, components: components,
methods: { methods: {

View File

@ -27,7 +27,7 @@
@endMove="() => $emit('endMove')"> @endMove="() => $emit('endMove')">
<object-frame v-if="domainObject" <object-frame v-if="domainObject"
:domain-object="domainObject" :domain-object="domainObject"
:object-path="objectPath" :object-path="currentObjectPath"
:has-frame="item.hasFrame" :has-frame="item.hasFrame"
:show-edit-view="false" :show-edit-view="false"
ref="objectFrame"> ref="objectFrame">
@ -71,7 +71,7 @@
hasFrame: hasFrameByDefault(domainObject.type) hasFrame: hasFrameByDefault(domainObject.type)
}; };
}, },
inject: ['openmct'], inject: ['openmct', 'objectPath'],
props: { props: {
item: Object, item: Object,
gridSize: Array, gridSize: Array,
@ -81,7 +81,7 @@
data() { data() {
return { return {
domainObject: undefined, domainObject: undefined,
objectPath: [] currentObjectPath: []
} }
}, },
components: { components: {
@ -100,7 +100,7 @@
methods: { methods: {
setObject(domainObject) { setObject(domainObject) {
this.domainObject = domainObject; this.domainObject = domainObject;
this.objectPath = [this.domainObject].concat(this.openmct.router.path); this.currentObjectPath = [this.domainObject].concat(this.objectPath.slice());
this.$nextTick(function () { this.$nextTick(function () {
let childContext = this.$refs.objectFrame.getSelectionContext(); let childContext = this.$refs.objectFrame.getSelectionContext();
childContext.item = domainObject; childContext.item = domainObject;

View File

@ -27,7 +27,8 @@
@endMove="() => $emit('endMove')"> @endMove="() => $emit('endMove')">
<div class="c-telemetry-view" <div class="c-telemetry-view"
:style="styleObject" :style="styleObject"
v-if="domainObject"> v-if="domainObject"
@contextmenu.prevent="showContextMenu">
<div v-if="showLabel" <div v-if="showLabel"
class="c-telemetry-view__label"> class="c-telemetry-view__label">
<div class="c-telemetry-view__label-text">{{ domainObject.name }}</div> <div class="c-telemetry-view__label-text">{{ domainObject.name }}</div>
@ -82,7 +83,8 @@
import printj from 'printj' import printj from 'printj'
const DEFAULT_TELEMETRY_DIMENSIONS = [10, 5], const DEFAULT_TELEMETRY_DIMENSIONS = [10, 5],
DEFAULT_POSITION = [1, 1]; DEFAULT_POSITION = [1, 1],
CONTEXT_MENU_ACTIONS = ['viewHistoricalData'];
export default { export default {
makeDefinition(openmct, gridSize, domainObject, position) { makeDefinition(openmct, gridSize, domainObject, position) {
@ -103,7 +105,7 @@
size: "13px" size: "13px"
}; };
}, },
inject: ['openmct'], inject: ['openmct', 'objectPath'],
props: { props: {
item: Object, item: Object,
gridSize: Array, gridSize: Array,
@ -163,7 +165,8 @@
return { return {
datum: undefined, datum: undefined,
formats: undefined, formats: undefined,
domainObject: undefined domainObject: undefined,
currentObjectPath: undefined
} }
}, },
watch: { watch: {
@ -218,12 +221,16 @@
}, },
setObject(domainObject) { setObject(domainObject) {
this.domainObject = domainObject; this.domainObject = domainObject;
this.keyString = this.openmct.objects.makeKeyString(domainObject.identifier);
this.metadata = this.openmct.telemetry.getMetadata(this.domainObject); this.metadata = this.openmct.telemetry.getMetadata(this.domainObject);
this.limitEvaluator = this.openmct.telemetry.limitEvaluator(this.domainObject); this.limitEvaluator = this.openmct.telemetry.limitEvaluator(this.domainObject);
this.formats = this.openmct.telemetry.getFormatMap(this.metadata); this.formats = this.openmct.telemetry.getFormatMap(this.metadata);
this.requestHistoricalData(); this.requestHistoricalData();
this.subscribeToObject(); this.subscribeToObject();
this.currentObjectPath = this.objectPath.slice();
this.currentObjectPath.unshift(this.domainObject);
this.context = { this.context = {
item: domainObject, item: domainObject,
layoutItem: this.item, layoutItem: this.item,
@ -235,6 +242,9 @@
}, },
updateTelemetryFormat(format) { updateTelemetryFormat(format) {
this.$emit('formatChanged', this.item, format); this.$emit('formatChanged', this.item, format);
},
showContextMenu(event) {
this.openmct.contextMenu._showContextMenuForObjectPath(this.currentObjectPath, event.x, event.y, CONTEXT_MENU_ACTIONS);
} }
}, },
mounted() { mounted() {

View File

@ -37,7 +37,7 @@ export default function DisplayLayoutPlugin(options) {
canEdit: function (domainObject) { canEdit: function (domainObject) {
return domainObject.type === 'layout'; return domainObject.type === 'layout';
}, },
view: function (domainObject) { view: function (domainObject, isEditing, objectPath) {
let component; let component;
return { return {
show(container) { show(container) {
@ -49,13 +49,14 @@ export default function DisplayLayoutPlugin(options) {
provide: { provide: {
openmct, openmct,
objectUtils, objectUtils,
options options,
objectPath
}, },
el: container, el: container,
data () { data () {
return { return {
domainObject: domainObject domainObject: domainObject
} };
} }
}); });
}, },

View File

@ -23,6 +23,7 @@
export default class GoToOriginalAction { export default class GoToOriginalAction {
constructor(openmct) { constructor(openmct) {
this.name = 'Go To Original'; this.name = 'Go To Original';
this.key = 'goToOriginal';
this.description = 'Go to the original unlinked instance of this object'; this.description = 'Go to the original unlinked instance of this object';
this._openmct = openmct; this._openmct = openmct;

View File

@ -115,10 +115,22 @@
width: (tickWidth + 30) + 'px' width: (tickWidth + 30) + 'px'
}"> }">
<div class="gl-plot-label gl-plot-y-label"> <div class="gl-plot-label gl-plot-y-label" ng-if="!yKeyOptions">
{{ yAxis.get('label') }} {{ yAxis.get('label') }}
</div> </div>
<div class="gl-plot-label gl-plot-y-label" ng-if="yKeyOptions.length > 1 && series.length === 1">
<select class="gl-plot-y-label__select"
ng-model="yAxisLabel" ng-change="plot.toggleYAxisLabel(yAxisLabel, yKeyOptions, series[0])">
<option ng-repeat="option in yKeyOptions"
value="{{option.name}}"
ng-selected="option.name == yAxisLabel">
{{option.name}}
</option>
</select>
</div>
<mct-ticks axis="yAxis"> <mct-ticks axis="yAxis">
<div ng-repeat="tick in ticks track by tick.text" <div ng-repeat="tick in ticks track by tick.text"
class="gl-plot-tick gl-plot-y-tick-label" class="gl-plot-tick gl-plot-y-tick-label"
@ -165,7 +177,7 @@
<div class="gl-plot__local-controls h-local-controls h-local-controls--overlay-content c-local-controls--show-on-hover"> <div class="gl-plot__local-controls h-local-controls h-local-controls--overlay-content c-local-controls--show-on-hover">
<div class="c-button-set c-button-set--strip-h"> <div class="c-button-set c-button-set--strip-h">
<button class="c-button icon-minus" <button class="c-button icon-minus"
ng-click="plot.zoom('out', 0.2)" ng-click="plot.toggleYAxis()"
title="Zoom out"> title="Zoom out">
</button> </button>
<button class="c-button icon-plus" <button class="c-button icon-plus"

View File

@ -93,6 +93,8 @@ define([
this.$scope.series = this.config.series.models; this.$scope.series = this.config.series.models;
this.$scope.legend = this.config.legend; this.$scope.legend = this.config.legend;
this.$scope.yAxisLabel = this.config.yAxis.get('label');
this.cursorGuideVertical = this.$element[0].querySelector('.js-cursor-guide--v'); this.cursorGuideVertical = this.$element[0].querySelector('.js-cursor-guide--v');
this.cursorGuideHorizontal = this.$element[0].querySelector('.js-cursor-guide--h'); this.cursorGuideHorizontal = this.$element[0].querySelector('.js-cursor-guide--h');
this.cursorGuide = false; this.cursorGuide = false;
@ -103,9 +105,27 @@ define([
this.listenTo(this.$scope, 'plot:tickWidth', this.onTickWidthChange, this); this.listenTo(this.$scope, 'plot:tickWidth', this.onTickWidthChange, this);
this.listenTo(this.$scope, 'plot:highlight:set', this.onPlotHighlightSet, this); this.listenTo(this.$scope, 'plot:highlight:set', this.onPlotHighlightSet, this);
this.listenTo(this.$scope, 'plot:reinitializeCanvas', this.initCanvas, this); this.listenTo(this.$scope, 'plot:reinitializeCanvas', this.initCanvas, this);
this.listenTo(this.config.xAxis, 'change:displayRange', this.onXAxisChange, this); this.listenTo(this.config.xAxis, 'change:displayRange', this.onXAxisChange, this);
this.listenTo(this.config.yAxis, 'change:displayRange', this.onYAxisChange, this); this.listenTo(this.config.yAxis, 'change:displayRange', this.onYAxisChange, this);
this.setUpYAxisOptions();
};
MCTPlotController.prototype.setUpYAxisOptions = function () {
if (this.$scope.series.length === 1) {
let metadata = this.$scope.series[0].metadata;
this.$scope.yKeyOptions = metadata
.valuesForHints(['range'])
.map(function (o) {
return {
name: o.name,
key: o.key
};
});
} else {
this.$scope.yKeyOptions = undefined;
}
}; };
MCTPlotController.prototype.onXAxisChange = function (displayBounds) { MCTPlotController.prototype.onXAxisChange = function (displayBounds) {
@ -493,5 +513,13 @@ define([
this.cursorGuide = !this.cursorGuide; this.cursorGuide = !this.cursorGuide;
}; };
MCTPlotController.prototype.toggleYAxisLabel = function (label, options, series) {
let yAxisObject = options.filter(o => o.name === label)[0];
if (yAxisObject) {
series.emit('change:yKey', yAxisObject.key);
}
};
return MCTPlotController; return MCTPlotController;
}); });

View File

@ -23,6 +23,7 @@
export default class RemoveAction { export default class RemoveAction {
constructor(openmct) { constructor(openmct) {
this.name = 'Remove'; this.name = 'Remove';
this.key = 'remove';
this.description = 'Remove this object from its containing object.'; this.description = 'Remove this object from its containing object.';
this.cssClass = "icon-trash"; this.cssClass = "icon-trash";

View File

@ -29,7 +29,7 @@ define([], function () {
this.limitEvaluator = limitEvaluator; this.limitEvaluator = limitEvaluator;
this.objectKeyString = objectKeyString; this.objectKeyString = objectKeyString;
} }
getFormattedDatum(headers) { getFormattedDatum(headers) {
return Object.keys(headers).reduce((formattedDatum, columnKey) => { return Object.keys(headers).reduce((formattedDatum, columnKey) => {
formattedDatum[columnKey] = this.getFormattedValue(columnKey); formattedDatum[columnKey] = this.getFormattedValue(columnKey);
@ -62,12 +62,16 @@ define([], function () {
this.cellLimitClasses = Object.values(this.columns).reduce((alarmStateMap, column) => { this.cellLimitClasses = Object.values(this.columns).reduce((alarmStateMap, column) => {
let limitEvaluation = this.limitEvaluator.evaluate(this.datum, column.getMetadatum()); let limitEvaluation = this.limitEvaluator.evaluate(this.datum, column.getMetadatum());
alarmStateMap[column.getKey()] = limitEvaluation && limitEvaluation.cssClass; alarmStateMap[column.getKey()] = limitEvaluation && limitEvaluation.cssClass;
return alarmStateMap; return alarmStateMap;
}, {}); }, {});
} }
return this.cellLimitClasses; return this.cellLimitClasses;
} }
getContextMenuActions() {
return [];
}
} }
/** /**
@ -85,4 +89,4 @@ define([], function () {
} }
return TelemetryTableRow; return TelemetryTableRow;
}); });

View File

@ -48,7 +48,7 @@ define([
canEdit(domainObject) { canEdit(domainObject) {
return domainObject.type === 'table'; return domainObject.type === 'table';
}, },
view(domainObject) { view(domainObject, isEditing, objectPath) {
let table = new TelemetryTable(domainObject, openmct); let table = new TelemetryTable(domainObject, openmct);
let component; let component;
return { return {
@ -64,7 +64,8 @@ define([
}, },
provide: { provide: {
openmct, openmct,
table table,
objectPath
}, },
el: element, el: element,
template: '<table-component :isEditing="isEditing" :enableMarking="true"></table-component>' template: '<table-component :isEditing="isEditing" :enableMarking="true"></table-component>'

View File

@ -26,7 +26,7 @@
rowClass, rowClass,
{'is-selected': marked} {'is-selected': marked}
]" ]"
@click="markRow"> v-on="listeners">
<component v-for="(title, key) in headers" <component v-for="(title, key) in headers"
:key="key" :key="key"
:is="componentList[key]" :is="componentList[key]"
@ -56,6 +56,7 @@
import TableCell from './table-cell.vue'; import TableCell from './table-cell.vue';
export default { export default {
inject: ['openmct', 'objectPath'],
data: function () { data: function () {
return { return {
rowTop: (this.rowOffset + this.rowIndex) * this.rowHeight + 'px', rowTop: (this.rowOffset + this.rowIndex) * this.rowHeight + 'px',
@ -150,6 +151,16 @@ export default {
}], false); }], false);
event.stopPropagation(); event.stopPropagation();
} }
},
showContextMenu: function (event) {
event.preventDefault();
this.openmct.objects.get(this.row.objectKeyString).then((domainObject) => {
let contextualObjectPath = this.objectPath.slice();
contextualObjectPath.unshift(domainObject);
this.openmct.contextMenu._showContextMenuForObjectPath(contextualObjectPath, event.x, event.y, this.row.getContextMenuActions());
});
} }
}, },
// TODO: use computed properties // TODO: use computed properties
@ -162,6 +173,19 @@ export default {
}, },
components: { components: {
TableCell TableCell
},
computed: {
listeners() {
let listenersObject = {
click: this.markRow
}
if (this.row.getContextMenuActions().length) {
listenersObject.contextmenu = this.showContextMenu;
}
return listenersObject;
}
} }
} }
</script> </script>

View File

@ -348,7 +348,7 @@ export default {
search, search,
TelemetryFilterIndicator TelemetryFilterIndicator
}, },
inject: ['table', 'openmct'], inject: ['table', 'openmct', 'objectPath'],
props: { props: {
isEditing: { isEditing: {
type: Boolean, type: Boolean,

View File

@ -211,6 +211,10 @@ $btnStdH: 24px;
$colorCursorGuide: rgba(white, 0.6); $colorCursorGuide: rgba(white, 0.6);
$shdwCursorGuide: rgba(black, 0.4) 0 0 2px; $shdwCursorGuide: rgba(black, 0.4) 0 0 2px;
$colorLocalControlOvrBg: rgba($colorBodyBg, 0.8); $colorLocalControlOvrBg: rgba($colorBodyBg, 0.8);
$colorSelectBg: $colorBtnBg; // This must be a solid color, not a gradient, due to usage of SVG bg in selects
$colorSelectFg: $colorBtnFg;
$colorSelectArw: lighten($colorBtnBg, 20%);
$shdwSelect: rgba(black, 0.5) 0 0.5px 3px;
// Menus // Menus
$colorMenuBg: pullForward($colorBodyBg, 15%); $colorMenuBg: pullForward($colorBodyBg, 15%);
@ -425,7 +429,3 @@ $createBtnTextTransform: uppercase;
background: linear-gradient(pullForward($c, 5%), $c); background: linear-gradient(pullForward($c, 5%), $c);
box-shadow: rgba(black, 0.5) 0 0.5px 2px; box-shadow: rgba(black, 0.5) 0 0.5px 2px;
} }
@mixin themedSelect($bg: $colorBtnBg, $fg: $colorBtnFg) {
@include cSelect(linear-gradient(lighten($bg, 5%), $bg), $fg, lighten($bg, 20%), rgba(black, 0.5) 0 0.5px 3px);
}

View File

@ -215,6 +215,10 @@ $btnStdH: 24px;
$colorCursorGuide: rgba(white, 0.6); $colorCursorGuide: rgba(white, 0.6);
$shdwCursorGuide: rgba(black, 0.4) 0 0 2px; $shdwCursorGuide: rgba(black, 0.4) 0 0 2px;
$colorLocalControlOvrBg: rgba($colorBodyBg, 0.8); $colorLocalControlOvrBg: rgba($colorBodyBg, 0.8);
$colorSelectBg: $colorBtnBg; // This must be a solid color, not a gradient, due to usage of SVG bg in selects
$colorSelectFg: $colorBtnFg;
$colorSelectArw: lighten($colorBtnBg, 20%);
$shdwSelect: rgba(black, 0.5) 0 0.5px 3px;
// Menus // Menus
$colorMenuBg: pullForward($colorBodyBg, 15%); $colorMenuBg: pullForward($colorBodyBg, 15%);
@ -430,10 +434,6 @@ $createBtnTextTransform: uppercase;
box-shadow: rgba(black, 0.5) 0 0.5px 2px; box-shadow: rgba(black, 0.5) 0 0.5px 2px;
} }
@mixin themedSelect($bg: $colorBtnBg, $fg: $colorBtnFg) {
@include cSelect(linear-gradient(lighten($bg, 5%), $bg), $fg, lighten($bg, 20%), rgba(black, 0.5) 0 0.5px 3px);
}
/**************************************************** OVERRIDES */ /**************************************************** OVERRIDES */
.c-frame { .c-frame {
&:not(.no-frame) { &:not(.no-frame) {

View File

@ -211,6 +211,10 @@ $btnStdH: 24px;
$colorCursorGuide: rgba(black, 0.6); $colorCursorGuide: rgba(black, 0.6);
$shdwCursorGuide: rgba(white, 0.4) 0 0 2px; $shdwCursorGuide: rgba(white, 0.4) 0 0 2px;
$colorLocalControlOvrBg: rgba($colorBodyFg, 0.8); $colorLocalControlOvrBg: rgba($colorBodyFg, 0.8);
$colorSelectBg: $colorBtnBg; // This must be a solid color, not a gradient, due to usage of SVG bg in selects
$colorSelectFg: $colorBtnFg;
$colorSelectArw: lighten($colorBtnBg, 20%);
$shdwSelect: none;
// Menus // Menus
$colorMenuBg: pushBack($colorBodyBg, 10%); $colorMenuBg: pushBack($colorBodyBg, 10%);
@ -424,7 +428,3 @@ $createBtnTextTransform: uppercase;
@mixin themedButton($c: $colorBtnBg) { @mixin themedButton($c: $colorBtnBg) {
background: $c; background: $c;
} }
@mixin themedSelect($bg: $colorBtnBg, $fg: $colorBtnFg) {
@include cSelect($bg, $fg, lighten($bg, 20%), none);
}

View File

@ -279,7 +279,10 @@ input[type=number]::-webkit-outer-spin-button {
// SELECTS // SELECTS
select { select {
@include appearanceNone(); @include appearanceNone();
@include themedSelect(); background-color: $colorSelectBg;
background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' width='10' height='10'%3e%3cpath fill='%23#{svgColorFromHex($colorSelectArw)}' d='M5 5l5-5H0z'/%3e%3c/svg%3e");
color: $colorSelectFg;
box-shadow: $shdwSelect;
background-repeat: no-repeat, no-repeat; background-repeat: no-repeat, no-repeat;
background-position: right .4em top 80%, 0 0; background-position: right .4em top 80%, 0 0;
border: none; border: none;

View File

@ -161,8 +161,7 @@ mct-plot {
height: auto; height: auto;
} }
&.gl-plot-y-label, &.gl-plot-y-label {
&.l-plot-y-label {
$x: -50%; $x: -50%;
$r: -90deg; $r: -90deg;
transform-origin: 50% 0; transform-origin: 50% 0;
@ -172,6 +171,12 @@ mct-plot {
left: 0; left: 0;
top: 50%; top: 50%;
white-space: nowrap; white-space: nowrap;
select {
background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' width='10' height='10'%3e%3cpath fill='%23#{svgColorFromHex($colorSelectArw)}' d='M0 5l5 5V0L0 5z'/%3e%3c/svg%3e");
background-position: left .4em top 50%, 0 0;
padding: 1px $interiorMargin 1px 20px;
}
} }
} }

View File

@ -585,6 +585,11 @@
} }
} }
@function svgColorFromHex($hexColor) {
// Remove initial # in color value
@return str-slice(inspect($hexColor), 2, str-length(inspect($hexColor)));
}
@mixin test($c: deeppink, $a: 0.3) { @mixin test($c: deeppink, $a: 0.3) {
background: rgba($c, $a) !important; background: rgba($c, $a) !important;
background-color: rgba($c, $a) !important; background-color: rgba($c, $a) !important;

View File

@ -44,7 +44,8 @@
class="c-so-view__object-view" class="c-so-view__object-view"
ref="objectView" ref="objectView"
:object="domainObject" :object="domainObject"
:show-edit-view="showEditView"> :show-edit-view="showEditView"
:object-path="objectPath">
</object-view> </object-view>
</div> </div>
</template> </template>

View File

@ -9,7 +9,8 @@ export default {
props: { props: {
view: String, view: String,
object: Object, object: Object,
showEditView: Boolean showEditView: Boolean,
objectPath: Array
}, },
destroyed() { destroyed() {
this.clear(); this.clear();
@ -91,17 +92,19 @@ export default {
return; return;
} }
let objectPath = this.currentObjectPath || this.objectPath;
if (provider.edit && this.showEditView) { if (provider.edit && this.showEditView) {
if (this.openmct.editor.isEditing()) { if (this.openmct.editor.isEditing()) {
this.currentView = provider.edit(this.currentObject); this.currentView = provider.edit(this.currentObject, true, objectPath);
} else { } else {
this.currentView = provider.view(this.currentObject, false); this.currentView = provider.view(this.currentObject, false, objectPath);
} }
this.openmct.editor.on('isEditing', this.toggleEditView); this.openmct.editor.on('isEditing', this.toggleEditView);
this.releaseEditModeHandler = () => this.openmct.editor.off('isEditing', this.toggleEditView); this.releaseEditModeHandler = () => this.openmct.editor.off('isEditing', this.toggleEditView);
} else { } else {
this.currentView = provider.view(this.currentObject, this.openmct.editor.isEditing()); this.currentView = provider.view(this.currentObject, this.openmct.editor.isEditing(), objectPath);
if (this.currentView.onEditModeChange) { if (this.currentView.onEditModeChange) {
this.openmct.editor.on('isEditing', this.invokeEditModeHandler); this.openmct.editor.on('isEditing', this.invokeEditModeHandler);
@ -117,7 +120,7 @@ export default {
this.openmct.objectViews.on('clearData', this.clearData); this.openmct.objectViews.on('clearData', this.clearData);
}, },
show(object, viewKey, immediatelySelect) { show(object, viewKey, immediatelySelect, currentObjectPath) {
if (this.unlisten) { if (this.unlisten) {
this.unlisten(); this.unlisten();
} }
@ -132,6 +135,11 @@ export default {
} }
this.currentObject = object; this.currentObject = object;
if (currentObjectPath) {
this.currentObjectPath = currentObjectPath;
}
this.unlisten = this.openmct.objects.observe(this.currentObject, '*', (mutatedObject) => { this.unlisten = this.openmct.objects.observe(this.currentObject, '*', (mutatedObject) => {
this.currentObject = mutatedObject; this.currentObject = mutatedObject;
}); });

View File

@ -19,28 +19,11 @@
</div> </div>
<div class="l-browse-bar__end"> <div class="l-browse-bar__end">
<div class="l-browse-bar__view-switcher c-ctrl-wrapper c-ctrl-wrapper--menus-left" <view-switcher
v-if="views.length > 1"> :currentView="currentView"
<button class="c-button--menu" :views="views"
:class="currentView.cssClass" @setView="setView">
title="Switch view type" </view-switcher>
@click.stop="toggleViewMenu">
<span class="c-button__label">
{{ currentView.name }}
</span>
</button>
<div class="c-menu" v-show="showViewMenu">
<ul>
<li v-for="(view, index) in views"
@click="setView(view)"
:key="index"
:class="view.cssClass"
:title="view.name">
{{ view.name }}
</li>
</ul>
</div>
</div>
<!-- Action buttons --> <!-- Action buttons -->
<div class="l-browse-bar__actions"> <div class="l-browse-bar__actions">
<button v-if="notebookEnabled" <button v-if="notebookEnabled"
@ -77,14 +60,15 @@
<script> <script>
import NotebookSnapshot from '../utils/notebook-snapshot'; import NotebookSnapshot from '../utils/notebook-snapshot';
import ViewSwitcher from './ViewSwitcher.vue';
const PLACEHOLDER_OBJECT = {}; const PLACEHOLDER_OBJECT = {};
export default { export default {
inject: ['openmct'], inject: ['openmct'],
components: {
ViewSwitcher
},
methods: { methods: {
toggleViewMenu() {
this.showViewMenu = !this.showViewMenu;
},
toggleSaveMenu() { toggleSaveMenu() {
this.showSaveMenu = !this.showSaveMenu; this.showSaveMenu = !this.showSaveMenu;
}, },

View File

@ -0,0 +1,55 @@
<template>
<div class="l-browse-bar__view-switcher c-ctrl-wrapper c-ctrl-wrapper--menus-left"
v-if="views.length > 1">
<button class="c-button--menu"
:class="currentView.cssClass"
title="Switch view type"
@click.stop="toggleViewMenu">
<span class="c-button__label">
{{ currentView.name }}
</span>
</button>
<div class="c-menu" v-show="showViewMenu">
<ul>
<li v-for="(view, index) in views"
@click="setView(view)"
:key="index"
:class="view.cssClass"
:title="view.name">
{{ view.name }}
</li>
</ul>
</div>
</div>
</template>
<script>
export default {
props: [
'currentView',
'views'
],
data() {
return {
showViewMenu: false
}
},
methods: {
setView(view) {
this.$emit('setView', view);
},
toggleViewMenu() {
this.showViewMenu = !this.showViewMenu;
},
hideViewMenu() {
this.showViewMenu = false;
}
},
mounted() {
document.addEventListener('click', this.hideViewMenu);
},
destroyed() {
document.removeEventListener('click', this.hideViewMenu);
}
}
</script>

View File

@ -33,6 +33,11 @@
</div> </div>
<div class="l-browse-bar__end"> <div class="l-browse-bar__end">
<div class="l-browse-bar__actions"> <div class="l-browse-bar__actions">
<view-switcher
:views="views"
:currentView="currentView"
@setView="setView">
</view-switcher>
<button v-if="notebookEnabled" <button v-if="notebookEnabled"
class="l-browse-bar__actions__edit c-button icon-notebook" class="l-browse-bar__actions__edit c-button icon-notebook"
title="New Notebook entry" title="New Notebook entry"
@ -80,20 +85,52 @@
<script> <script>
import ContextMenuDropDown from '../../ui/components/contextMenuDropDown.vue'; import ContextMenuDropDown from '../../ui/components/contextMenuDropDown.vue';
import ViewSwitcher from '../../ui/layout/ViewSwitcher.vue';
import NotebookSnapshot from '../utils/notebook-snapshot'; import NotebookSnapshot from '../utils/notebook-snapshot';
export default { export default {
components: { components: {
ContextMenuDropDown ContextMenuDropDown,
ViewSwitcher
}, },
inject: [ inject: [
'openmct', 'openmct',
'objectPath' 'objectPath'
], ],
computed: {
views() {
return this
.openmct
.objectViews
.get(this.domainObject);
},
currentView() {
return this.views.filter(v => v.key === this.viewKey)[0] || {};
}
},
methods: { methods: {
snapshot() { snapshot() {
let element = document.getElementsByClassName("l-preview-window__object-view")[0]; let element = document.getElementsByClassName("l-preview-window__object-view")[0];
this.notebookSnapshot.capture(this.domainObject, element); this.notebookSnapshot.capture(this.domainObject, element);
},
clear() {
if (this.view) {
this.view.destroy();
this.$refs.objectView.innerHTML = '';
}
delete this.view;
delete this.viewContainer;
},
setView(view) {
this.clear();
this.viewKey = view.key;
this.viewContainer = document.createElement('div');
this.viewContainer.classList.add('c-object-view','u-contents');
this.$refs.objectView.append(this.viewContainer);
this.view = this.currentView.view(this.domainObject, false, this.objectPath);
this.view.show(this.viewContainer, false);
} }
}, },
data() { data() {
@ -103,13 +140,13 @@
return { return {
domainObject: domainObject, domainObject: domainObject,
type: type, type: type,
notebookEnabled: false notebookEnabled: false,
viewKey: undefined
}; };
}, },
mounted() { mounted() {
let viewProvider = this.openmct.objectViews.get(this.domainObject)[0]; let view = this.openmct.objectViews.get(this.domainObject)[0];
this.view = viewProvider.view(this.domainObject); this.setView(view);
this.view.show(this.$refs.objectView, false);
if (this.openmct.types.get('notebook')) { if (this.openmct.types.get('notebook')) {
this.notebookSnapshot = new NotebookSnapshot(this.openmct); this.notebookSnapshot = new NotebookSnapshot(this.openmct);

View File

@ -28,6 +28,7 @@ export default class PreviewAction {
* Metadata * Metadata
*/ */
this.name = 'Preview'; this.name = 'Preview';
this.key = 'preview';
this.description = 'Preview in large dialog'; this.description = 'Preview in large dialog';
this.cssClass = 'icon-eye-open'; this.cssClass = 'icon-eye-open';

View File

@ -0,0 +1,35 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2018, 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 PreviewAction from './PreviewAction';
export default class ViewHistoricalDataAction extends PreviewAction {
constructor(openmct) {
super(openmct);
this.name = 'View Historical Data';
this.key = 'viewHistoricalData';
this.description = 'View Historical Data in a Table or Plot';
this.cssClass = 'icon-eye-open';
this.hideInDefaultMenu = true;
}
}

View File

@ -20,9 +20,11 @@
* at runtime from the About dialog for additional information. * at runtime from the About dialog for additional information.
*****************************************************************************/ *****************************************************************************/
import PreviewAction from './PreviewAction.js'; import PreviewAction from './PreviewAction.js';
import ViewHistoricalDataAction from './ViewHistoricalDataAction';
export default function () { export default function () {
return function (openmct) { return function (openmct) {
openmct.contextMenu.registerAction(new PreviewAction(openmct)); openmct.contextMenu.registerAction(new PreviewAction(openmct));
} openmct.contextMenu.registerAction(new ViewHistoricalDataAction(openmct));
};
} }

View File

@ -223,11 +223,11 @@ define(['EventEmitter'], function (EventEmitter) {
/** /**
* Provide a view of this object. * Provide a view of this object.
* *
* When called by Open MCT, this may include additional arguments * When called by Open MCT, the following arguments will be passed to it:
* which are on the path to the object to be viewed; for instance, * @param {object} domainObject - the domainObject that the view is provided for
* when viewing "A Folder" within "My Items", this method will be * @param {boolean} isEditing - A boolean value indicating wether openmct is in a global edit mode
* invoked with "A Folder" (as a domain object) as the first argument, * @param {array} objectPath - The current contextual object path of the view object
* and "My Items" as the second argument. * eg current domainObject is located under MyItems which is under Root
* *
* @method view * @method view
* @memberof module:openmct.ViewProvider# * @memberof module:openmct.ViewProvider#

View File

@ -8,6 +8,7 @@ define([
let navigateCall = 0; let navigateCall = 0;
let browseObject; let browseObject;
let unobserve = undefined; let unobserve = undefined;
let currentObjectPath;
openmct.router.route(/^\/browse\/?$/, navigateToFirstChildOfRoot); openmct.router.route(/^\/browse\/?$/, navigateToFirstChildOfRoot);
@ -26,7 +27,9 @@ define([
}); });
function viewObject(object, viewProvider) { function viewObject(object, viewProvider) {
openmct.layout.$refs.browseObject.show(object, viewProvider.key, true); currentObjectPath = openmct.router.path;
openmct.layout.$refs.browseObject.show(object, viewProvider.key, true, currentObjectPath);
openmct.layout.$refs.browseBar.domainObject = object; openmct.layout.$refs.browseBar.domainObject = object;
openmct.layout.$refs.browseBar.viewKey = viewProvider.key; openmct.layout.$refs.browseBar.viewKey = viewProvider.key;
} }