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
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
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;

View File

@ -55,16 +55,19 @@ define(
navigatedObject = this.navigationService.getNavigation(),
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 (selectedObject.hasCapability("editor") && selectedObject.getCapability("editor").inEditContext()) {
return this.editModeBlacklist.indexOf(actionMetadata.key) === -1;
} else {
//Target is in the context menu
return this.nonEditContextBlacklist.indexOf(actionMetadata.key) === -1;
}
// if (selectedObject.hasCapability("editor") && selectedObject.getCapability("editor").inEditContext()) {
// return this.editModeBlacklist.indexOf(actionMetadata.key) === -1;
// } else {
// //Target is in the context menu
// return this.nonEditContextBlacklist.indexOf(actionMetadata.key) === -1;
// }
// } else {
// return true;
// }
return true;
};
return EditContextualActionPolicy;

View File

@ -41,6 +41,7 @@ define([
'./styles-new/core.scss',
'./styles-new/notebook.scss',
'./ui/components/layout/Layout.vue',
'../platform/core/src/capabilities/ContextualDomainObject',
'vue'
], function (
EventEmitter,
@ -63,6 +64,7 @@ define([
coreStyles,
NotebookStyles,
Layout,
ContextualDomainObject,
Vue
) {
/**
@ -241,6 +243,34 @@ define([
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.
* @memberof module:openmct.MCT#

View File

@ -177,7 +177,11 @@ define([
CompositionCollection.prototype.load = function () {
return this.provider.load(this.domainObject)
.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))
.then(function (children) {
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>
<div class="l-grid-view">
<div v-for="(item, index) in items"
v-bind:key="index"
class="l-grid-view__item c-grid-item"
:class="{ 'is-alias': item.isAlias === true }"
@click="navigate(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>
<grid-item v-for="(item, index) in items"
:key="index"
:item="item"
:object-path="item.objectPath">
</grid-item>
</div>
</template>
@ -177,17 +159,11 @@
<script>
import compositionLoader from './composition-loader';
import GridItem from './GridItem.vue';
export default {
components: {GridItem},
mixins: [compositionLoader],
inject: ['domainObject', '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);
}
}
inject: ['openmct']
}
</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">
<table class="c-table__body">
<thead class="c-table__header">
<tr>
<th class="is-sortable"
:class="{
'is-sorting': sortBy === 'model.name',
'asc': ascending,
'desc': !ascending
}"
@click="sort('model.name', true)">
Name
</th>
<th class="is-sortable"
:class="{
'is-sorting': sortBy === 'type.name',
'asc': ascending,
'desc': !ascending
}"
@click="sort('type.name', true)">
Type
</th>
<th class="is-sortable"
:class="{
'is-sorting': sortBy === 'model.persisted',
'asc': ascending,
'desc': !ascending
}"
@click="sort('model.persisted', false)">
Created Date
</th>
<th class="is-sortable"
:class="{
'is-sorting': sortBy === 'model.modified',
'asc': ascending,
'desc': !ascending
}"
@click="sort('model.modified', false)">
Updated Date
</th>
</tr>
<tr>
<th class="is-sortable"
:class="{
'is-sorting': sortBy === 'model.name',
'asc': ascending,
'desc': !ascending
}"
@click="sort('model.name', true)">
Name
</th>
<th class="is-sortable"
:class="{
'is-sorting': sortBy === 'type.name',
'asc': ascending,
'desc': !ascending
}"
@click="sort('type.name', true)">
Type
</th>
<th class="is-sortable"
:class="{
'is-sorting': sortBy === 'model.persisted',
'asc': ascending,
'desc': !ascending
}"
@click="sort('model.persisted', false)">
Created Date
</th>
<th class="is-sortable"
:class="{
'is-sorting': sortBy === 'model.modified',
'asc': ascending,
'desc': !ascending
}"
@click="sort('model.modified', false)">
Updated Date
</th>
</tr>
</thead>
<tbody>
<tr class="c-list-item"
v-for="(item,index) in sortedItems"
v-bind:key="index"
:class="{ 'is-alias': item.isAlias === true }"
@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>
<list-item v-for="(item,index) in sortedItems"
:item="item"
:object-path="item.objectPath">
</list-item>
</tbody>
</table>
</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>
<script>
import lodash from 'lodash';
import moment from 'moment';
import compositionLoader from './composition-loader';
import ListItem from './ListItem.vue';
export default {
components: {ListItem},
mixins: [compositionLoader],
inject: ['domainObject', 'openmct'],
data() {
@ -156,15 +114,6 @@ export default {
}
},
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) {
if (this.sortBy === field) {
this.ascending = !this.ascending;

View File

@ -31,16 +31,19 @@ export default {
methods: {
add(child, index, anything) {
var type = this.openmct.types.get(child.type) || unknownObjectType;
this.items.push({
model: child,
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) {
// TODO: implement remove action
console.log('remove child? might be identifier');
remove(identifier) {
this.items = this.items
.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"
draggable="true"
@dragstart="dragStart"
:href="urlLink">
:href="objectLink">
<div class="c-tree__item__type-icon"
:class="cssClass"></div>
:class="typeClass"></div>
<div class="c-tree__item__name">{{ domainObject.name }}</div>
</a>
</template>
<script>
import ContextMenu from '../mixins/context-menu';
import ObjectLink from '../mixins/object-link';
export default {
mixins: [ContextMenu, ObjectLink],
inject: ['openmct'],
props: {
'domainObject': Object,
'path': Array
},
computed: {
urlLink() {
if (!this.path) {
return;
typeClass() {
let type = this.openmct.types.get(this.domainObject.type);
if (!type) {
return 'icon-object-unknown';
}
return '#/browse/' + this.path
.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);
return type.definition.cssClass;
}
},
methods: {
dragStart(event) {
event.dataTransfer.setData("domainObject", JSON.stringify(this.domainObject));
}
},
destroyed() {
}
}
</script>

View File

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

View File

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