context menu and shared object link generation (#2199)

* temporarily disable remove dialog which is broken

* temporarily remove broken context action policy

* let openmct generate legacy objects

* ensure composition loads in specified order

* redo nav and add context menu support to tree

* componentize grid and list view, add context menus
This commit is contained in:
Pete Richards
2018-11-08 17:21:18 -08:00
committed by GitHub
parent 1069a45cfc
commit ff7df9ad1e
14 changed files with 417 additions and 171 deletions

View File

@ -69,8 +69,8 @@ define([], function () {
} }
] ]
}; };
setTimeout(() => this.removeCallback(domainObject));
dialog = this.dialogService.showBlockingMessage(model);
}; };
return RemoveDialog; return RemoveDialog;

View File

@ -55,16 +55,19 @@ define(
navigatedObject = this.navigationService.getNavigation(), navigatedObject = this.navigationService.getNavigation(),
actionMetadata = action.getMetadata ? action.getMetadata() : {}; actionMetadata = action.getMetadata ? action.getMetadata() : {};
// FIXME: need to restore support for changing contextual actions
// based on edit mode.
// if (navigatedObject.hasCapability("editor") && navigatedObject.getCapability("editor").isEditContextRoot()) { // if (navigatedObject.hasCapability("editor") && navigatedObject.getCapability("editor").isEditContextRoot()) {
if (selectedObject.hasCapability("editor") && selectedObject.getCapability("editor").inEditContext()) { // if (selectedObject.hasCapability("editor") && selectedObject.getCapability("editor").inEditContext()) {
return this.editModeBlacklist.indexOf(actionMetadata.key) === -1; // return this.editModeBlacklist.indexOf(actionMetadata.key) === -1;
} else { // } else {
//Target is in the context menu // //Target is in the context menu
return this.nonEditContextBlacklist.indexOf(actionMetadata.key) === -1; // return this.nonEditContextBlacklist.indexOf(actionMetadata.key) === -1;
} // }
// } else { // } else {
// return true; // return true;
// } // }
return true;
}; };
return EditContextualActionPolicy; return EditContextualActionPolicy;

View File

@ -41,6 +41,7 @@ define([
'./styles-new/core.scss', './styles-new/core.scss',
'./styles-new/notebook.scss', './styles-new/notebook.scss',
'./ui/components/layout/Layout.vue', './ui/components/layout/Layout.vue',
'../platform/core/src/capabilities/ContextualDomainObject',
'vue' 'vue'
], function ( ], function (
EventEmitter, EventEmitter,
@ -63,6 +64,7 @@ define([
coreStyles, coreStyles,
NotebookStyles, NotebookStyles,
Layout, Layout,
ContextualDomainObject,
Vue Vue
) { ) {
/** /**
@ -241,6 +243,34 @@ define([
this.legacyBundle.extensions[category].push(extension); this.legacyBundle.extensions[category].push(extension);
}; };
/**
* Return a legacy object, for compatibility purposes only. This method
* will be deprecated and removed in the future.
* @private
*/
MCT.prototype.legacyObject = function (domainObject) {
if (Array.isArray(domainObject)) {
// an array of domain objects. [object, ...ancestors] representing
// a single object with a given chain of ancestors. We instantiate
// as a single contextual domain object.
return domainObject
.map((o) => {
let keyString = objectUtils.makeKeyString(o.identifier);
let oldModel = objectUtils.toOldFormat(o);
return this.$injector.get('instantiate')(oldModel, keyString);
})
.reverse()
.reduce((parent, child) => {
return new ContextualDomainObject(child, parent);
});
} else {
let keyString = objectUtils.makeKeyString(domainObject.identifier);
let oldModel = objectUtils.toOldFormat(domainObject);
return this.$injector.get('instantiate')(oldModel, keyString);
}
};
/** /**
* Set path to where assets are hosted. This should be the path to main.js. * Set path to where assets are hosted. This should be the path to main.js.
* @memberof module:openmct.MCT# * @memberof module:openmct.MCT#

View File

@ -177,7 +177,11 @@ define([
CompositionCollection.prototype.load = function () { CompositionCollection.prototype.load = function () {
return this.provider.load(this.domainObject) return this.provider.load(this.domainObject)
.then(function (children) { .then(function (children) {
return Promise.all(children.map(this.onProviderAdd, this)); return Promise.all(children.map((c) => this.publicAPI.objects.get(c)));
}.bind(this))
.then(function (childObjects) {
childObjects.forEach(c => this.add(c, true));
return childObjects;
}.bind(this)) }.bind(this))
.then(function (children) { .then(function (children) {
this.emit('load'); this.emit('load');

View File

@ -0,0 +1,160 @@
<template>
<a class="l-grid-view__item c-grid-item"
:class="{ 'is-alias': item.isAlias === true }"
:href="objectLink">
<div class="c-grid-item__type-icon"
:class="(item.type.cssClass != undefined) ? 'bg-' + item.type.cssClass : 'bg-icon-object-unknown'">
</div>
<div class="c-grid-item__details">
<!-- Name and metadata -->
<div class="c-grid-item__name"
:title="item.model.name">{{item.model.name}}</div>
<div class="c-grid-item__metadata"
:title="item.type.name">
<span class="c-grid-item__metadata__type">{{item.type.name}}</span>
</div>
</div>
<div class="c-grid-item__controls">
<div class="icon-people" title='Shared'></div>
<button class="c-click-icon icon-info c-info-button" title='More Info'></button>
<div class="icon-pointer-right c-pointer-icon"></div>
</div>
</a>
</template>
<style lang="scss">
@import "~styles/sass-base";
/******************************* GRID ITEMS */
.c-grid-item {
// Mobile-first
@include button($bg: $colorItemBg, $fg: $colorItemFg);
cursor: pointer;
display: flex;
padding: $interiorMarginLg;
&__type-icon {
filter: $colorKeyFilter;
flex: 0 0 $gridItemMobile;
font-size: floor($gridItemMobile / 2);
margin-right: $interiorMarginLg;
}
&.is-alias {
// Object is an alias to an original.
[class*='__type-icon'] {
@include isAlias();
color: $colorIconAliasForKeyFilter;
}
}
&__details {
display: flex;
flex-flow: column nowrap;
flex: 1 1 auto;
}
&__name {
@include ellipsize();
color: $colorItemFg;
font-size: 1.2em;
font-weight: 400;
margin-bottom: $interiorMarginSm;
}
&__metadata {
color: $colorItemFgDetails;
font-size: 0.9em;
body.mobile & {
[class*='__item-count'] {
&:before {
content: ' - ';
}
}
}
}
&__controls {
color: $colorItemFgDetails;
flex: 0 0 64px;
font-size: 1.2em;
display: flex;
align-items: center;
justify-content: flex-end;
> * + * {
margin-left: $interiorMargin;
}
}
body.desktop & {
$transOutMs: 300ms;
flex-flow: column nowrap;
transition: background $transOutMs ease-in-out;
&:hover {
background: $colorItemBgHov;
transition: $transIn;
.c-grid-item__type-icon {
filter: $colorKeyFilterHov;
transform: scale(1);
transition: $transInBounce;
}
}
> * {
margin: 0; // Reset from mobile
}
&__controls {
align-items: start;
flex: 0 0 auto;
order: 1;
.c-info-button,
.c-pointer-icon { display: none; }
}
&__type-icon {
flex: 1 1 auto;
font-size: floor($gridItemDesk / 3);
margin: $interiorMargin 22.5% $interiorMargin * 3 22.5%;
order: 2;
transform: scale(0.9);
transform-origin: center;
transition: all $transOutMs ease-in-out;
}
&__details {
flex: 0 0 auto;
justify-content: flex-end;
order: 3;
}
&__metadata {
display: flex;
&__type {
flex: 1 1 auto;
@include ellipsize();
}
&__item-count {
opacity: 0.7;
flex: 0 0 auto;
}
}
}
}
</style>
<script>
import contextMenu from '../../../ui/components/mixins/context-menu';
import objectLink from '../../../ui/components/mixins/object-link';
export default {
mixins: [contextMenu, objectLink],
props: ['item']
}
</script>

View File

@ -1,28 +1,10 @@
<template> <template>
<div class="l-grid-view"> <div class="l-grid-view">
<div v-for="(item, index) in items" <grid-item v-for="(item, index) in items"
v-bind:key="index" :key="index"
class="l-grid-view__item c-grid-item" :item="item"
:class="{ 'is-alias': item.isAlias === true }" :object-path="item.objectPath">
@click="navigate(item)"> </grid-item>
<div class="c-grid-item__type-icon"
:class="(item.type.cssClass != undefined) ? 'bg-' + item.type.cssClass : 'bg-icon-object-unknown'">
</div>
<div class="c-grid-item__details">
<!-- Name and metadata -->
<div class="c-grid-item__name"
:title="item.model.name">{{item.model.name}}</div>
<div class="c-grid-item__metadata"
:title="item.type.name">
<span class="c-grid-item__metadata__type">{{item.type.name}}</span>
</div>
</div>
<div class="c-grid-item__controls">
<div class="icon-people" title='Shared'></div>
<button class="c-click-icon icon-info c-info-button" title='More Info'></button>
<div class="icon-pointer-right c-pointer-icon"></div>
</div>
</div>
</div> </div>
</template> </template>
@ -177,17 +159,11 @@
<script> <script>
import compositionLoader from './composition-loader'; import compositionLoader from './composition-loader';
import GridItem from './GridItem.vue';
export default { export default {
components: {GridItem},
mixins: [compositionLoader], mixins: [compositionLoader],
inject: ['domainObject', 'openmct'], inject: ['openmct']
methods: {
navigate(item) {
let currentLocation = this.openmct.router.currentLocation.path,
navigateToPath = `${currentLocation}/${this.openmct.objects.makeKeyString(item.model.identifier)}`;
this.openmct.router.setPath(navigateToPath);
}
}
} }
</script> </script>

View File

@ -0,0 +1,72 @@
<template>
<tr class="c-list-item"
:class="{ 'is-alias': item.isAlias === true }"
@click="navigate">
<td class="c-list-item__name">
<a :href="objectLink" ref="objectLink">
<div class="c-list-item__type-icon"
:class="item.type.cssClass"></div>
{{item.model.name}}
</a>
</td>
<td class="c-list-item__type">{{ item.type.name }}</td>
<td class="c-list-item__date-created">{{ formatTime(item.model.persisted, 'YYYY-MM-DD HH:mm:ss:SSS') }}Z</td>
<td class="c-list-item__date-updated">{{ formatTime(item.model.modified, 'YYYY-MM-DD HH:mm:ss:SSS') }}Z</td>
</tr>
</template>
<style lang="scss">
@import "~styles/sass-base";
/******************************* LIST ITEM */
.c-list-item {
&__name {
@include ellipsize();
}
&__type-icon {
color: $colorKey;
display: inline-block;
width: 1em;
margin-right:$interiorMarginSm;
}
&.is-alias {
// Object is an alias to an original.
[class*='__type-icon'] {
&:after {
color: $colorIconAlias;
content: $glyph-icon-link;
font-family: symbolsfont;
display: block;
position: absolute;
text-shadow: rgba(black, 0.5) 0 1px 2px;
top: auto; left: -1px; bottom: 1px; right: auto;
transform-origin: bottom left;
transform: scale(0.65);
}
}
}
}
</style>
<script>
import moment from 'moment';
import contextMenu from '../../../ui/components/mixins/context-menu';
import objectLink from '../../../ui/components/mixins/object-link';
export default {
mixins: [contextMenu, objectLink],
props: ['item'],
methods: {
formatTime(timestamp, format) {
return moment(timestamp).format(format);
},
navigate() {
this.$refs.objectLink.click();
}
}
}
</script>

View File

@ -2,60 +2,50 @@
<div class="c-table c-table--sortable c-list-view"> <div class="c-table c-table--sortable c-list-view">
<table class="c-table__body"> <table class="c-table__body">
<thead class="c-table__header"> <thead class="c-table__header">
<tr> <tr>
<th class="is-sortable" <th class="is-sortable"
:class="{ :class="{
'is-sorting': sortBy === 'model.name', 'is-sorting': sortBy === 'model.name',
'asc': ascending, 'asc': ascending,
'desc': !ascending 'desc': !ascending
}" }"
@click="sort('model.name', true)"> @click="sort('model.name', true)">
Name Name
</th> </th>
<th class="is-sortable" <th class="is-sortable"
:class="{ :class="{
'is-sorting': sortBy === 'type.name', 'is-sorting': sortBy === 'type.name',
'asc': ascending, 'asc': ascending,
'desc': !ascending 'desc': !ascending
}" }"
@click="sort('type.name', true)"> @click="sort('type.name', true)">
Type Type
</th> </th>
<th class="is-sortable" <th class="is-sortable"
:class="{ :class="{
'is-sorting': sortBy === 'model.persisted', 'is-sorting': sortBy === 'model.persisted',
'asc': ascending, 'asc': ascending,
'desc': !ascending 'desc': !ascending
}" }"
@click="sort('model.persisted', false)"> @click="sort('model.persisted', false)">
Created Date Created Date
</th> </th>
<th class="is-sortable" <th class="is-sortable"
:class="{ :class="{
'is-sorting': sortBy === 'model.modified', 'is-sorting': sortBy === 'model.modified',
'asc': ascending, 'asc': ascending,
'desc': !ascending 'desc': !ascending
}" }"
@click="sort('model.modified', false)"> @click="sort('model.modified', false)">
Updated Date Updated Date
</th> </th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr class="c-list-item" <list-item v-for="(item,index) in sortedItems"
v-for="(item,index) in sortedItems" :item="item"
v-bind:key="index" :object-path="item.objectPath">
:class="{ 'is-alias': item.isAlias === true }" </list-item>
@click="navigate(item)">
<td class="c-list-item__name">
<div class="c-list-item__type-icon"
:class="item.type.cssClass"></div>
{{item.model.name}}
</td>
<td class="c-list-item__type">{{ item.type.name }}</td>
<td class="c-list-item__date-created">{{ formatTime(item.model.persisted, 'YYYY-MM-DD HH:mm:ss:SSS') }}Z</td>
<td class="c-list-item__date-updated">{{ formatTime(item.model.modified, 'YYYY-MM-DD HH:mm:ss:SSS') }}Z</td>
</tr>
</tbody> </tbody>
</table> </table>
</div> </div>
@ -96,48 +86,16 @@
} }
} }
} }
.c-list-item {
&__name {
@include ellipsize();
}
&__type-icon {
color: $colorKey;
display: inline-block;
width: 1em;
margin-right:$interiorMarginSm;
}
&.is-alias {
// Object is an alias to an original.
[class*='__type-icon'] {
&:after {
color: $colorIconAlias;
content: $glyph-icon-link;
font-family: symbolsfont;
display: block;
position: absolute;
text-shadow: rgba(black, 0.5) 0 1px 2px;
top: auto; left: -1px; bottom: 1px; right: auto;
transform-origin: bottom left;
transform: scale(0.65);
}
}
}
}
/******************************* LIST ITEM */
</style> </style>
<script> <script>
import lodash from 'lodash'; import lodash from 'lodash';
import moment from 'moment';
import compositionLoader from './composition-loader'; import compositionLoader from './composition-loader';
import ListItem from './ListItem.vue';
export default { export default {
components: {ListItem},
mixins: [compositionLoader], mixins: [compositionLoader],
inject: ['domainObject', 'openmct'], inject: ['domainObject', 'openmct'],
data() { data() {
@ -156,15 +114,6 @@ export default {
} }
}, },
methods: { methods: {
formatTime(timestamp, format) {
return moment(timestamp).format(format);
},
navigate(item) {
let currentLocation = this.openmct.router.currentLocation.path,
navigateToPath = `${currentLocation}/${this.openmct.objects.makeKeyString(item.model.identifier)}`;
this.openmct.router.setPath(navigateToPath);
},
sort(field, defaultDirection) { sort(field, defaultDirection) {
if (this.sortBy === field) { if (this.sortBy === field) {
this.ascending = !this.ascending; this.ascending = !this.ascending;

View File

@ -31,16 +31,19 @@ export default {
methods: { methods: {
add(child, index, anything) { add(child, index, anything) {
var type = this.openmct.types.get(child.type) || unknownObjectType; var type = this.openmct.types.get(child.type) || unknownObjectType;
this.items.push({ this.items.push({
model: child, model: child,
type: type.definition, type: type.definition,
isAlias: this.domainObject.identifier.key !== child.location isAlias: this.domainObject.identifier.key !== child.location,
objectPath: [child].concat(openmct.router.path)
}); });
}, },
remove(child) { remove(identifier) {
// TODO: implement remove action this.items = this.items
console.log('remove child? might be identifier'); .filter((i) => {
return i.model.identifier.key !== identifier.key
|| i.model.identifier.namespace !== identifier.namespace
});
} }
} }
} }

View File

@ -2,51 +2,37 @@
<a class="c-tree__item__label" <a class="c-tree__item__label"
draggable="true" draggable="true"
@dragstart="dragStart" @dragstart="dragStart"
:href="urlLink"> :href="objectLink">
<div class="c-tree__item__type-icon" <div class="c-tree__item__type-icon"
:class="cssClass"></div> :class="typeClass"></div>
<div class="c-tree__item__name">{{ domainObject.name }}</div> <div class="c-tree__item__name">{{ domainObject.name }}</div>
</a> </a>
</template> </template>
<script> <script>
import ContextMenu from '../mixins/context-menu';
import ObjectLink from '../mixins/object-link';
export default { export default {
mixins: [ContextMenu, ObjectLink],
inject: ['openmct'], inject: ['openmct'],
props: { props: {
'domainObject': Object, 'domainObject': Object,
'path': Array
}, },
computed: { computed: {
urlLink() { typeClass() {
if (!this.path) { let type = this.openmct.types.get(this.domainObject.type);
return; if (!type) {
return 'icon-object-unknown';
} }
return '#/browse/' + this.path return type.definition.cssClass;
.map(o => this.openmct.objects.makeKeyString(o))
.join('/');
}
},
data() {
return {
cssClass: 'icon-object-unknown'
}
},
mounted() {
let type = this.openmct.types.get(this.domainObject.type);
if (type.definition.cssClass) {
this.cssClass = type.definition.cssClass;
} else {
console.log("Failed to get typeDef.cssClass for object", this.domainObject.name, this.domainObject.type);
} }
}, },
methods: { methods: {
dragStart(event) { dragStart(event) {
event.dataTransfer.setData("domainObject", JSON.stringify(this.domainObject)); event.dataTransfer.setData("domainObject", JSON.stringify(this.domainObject));
} }
},
destroyed() {
} }
} }
</script> </script>

View File

@ -114,7 +114,7 @@
return { return {
id: this.openmct.objects.makeKeyString(c.identifier), id: this.openmct.objects.makeKeyString(c.identifier),
object: c, object: c,
path: [c.identifier] objectPath: [c]
}; };
})) }))
}, },

View File

@ -7,7 +7,9 @@
:expanded="expanded" :expanded="expanded"
@click="toggleChildren"> @click="toggleChildren">
</view-control> </view-control>
<object-label :domainObject="node.object" :path="node.path"></object-label> <object-label :domainObject="node.object"
:objectPath="node.objectPath">
</object-label>
</div> </div>
<ul v-if="expanded" class="c-tree"> <ul v-if="expanded" class="c-tree">
<tree-item v-for="child in children" <tree-item v-for="child in children"
@ -76,12 +78,13 @@
this.children.push({ this.children.push({
id: this.openmct.objects.makeKeyString(child.identifier), id: this.openmct.objects.makeKeyString(child.identifier),
object: child, object: child,
path: this.node.path.concat([child.identifier]) objectPath: [child].concat(this.node.objectPath)
}); });
}, },
removeChild(child) { removeChild(identifier) {
// TODO: remove child on remove event. let removeId = this.openmct.objects.makeKeyString(identifier);
console.log('Tree should remove child', child); this.children = this.children
.filter(c => c.id !== removeId);
}, },
finishLoading () { finishLoading () {
this.isLoading = false; this.isLoading = false;

View File

@ -0,0 +1,38 @@
export default {
inject: ['openmct'],
props: {
'objectPath': {
type: Array,
default() {
return [];
}
}
},
mounted() {
// TODO: handle mobile contet menu listeners.
this.$el.addEventListener('contextmenu', this.showContextMenu);
this.objectPath.forEach((o, i) => {
let removeListener = this.openmct.objects.observe(
o,
'*',
(newDomainObject) => {
this.objectPath.splice(i, 1, newDomainObject);
}
);
this.$once('hook:destroyed', removeListener);
});
},
destroyed() {
this.$el.removeEventListener('contextmenu', this.showContextMenu);
},
methods: {
showContextMenu(event) {
let legacyObject = this.openmct.legacyObject(this.objectPath);
legacyObject.getCapability('action').perform({
key: 'menu',
domainObject: legacyObject,
event: event
});
}
}
};

View File

@ -0,0 +1,22 @@
export default {
inject: ['openmct'],
props: {
'objectPath': {
type: Array,
default() {
return [];
}
}
},
computed: {
objectLink() {
if (!this.objectPath.length) {
return;
}
return '#/browse/' + this.objectPath
.map(o => this.openmct.objects.makeKeyString(o.identifier))
.reverse()
.join('/');
}
}
};