Object views (#2165)

* Use new style view providers for all object views.

* support legacy views with deprecation warning

* tidy deprecation warnings
This commit is contained in:
Pete Richards 2018-09-13 18:37:20 -07:00 committed by GitHub
parent 07ca60e13a
commit 40b7117987
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 331 additions and 371 deletions

View File

@ -35,6 +35,7 @@ define([
'./ui/registries/InspectorViewRegistry',
'./ui/registries/ToolbarRegistry',
'./ui/router/ApplicationRouter',
'./ui/router/Browse',
'../platform/framework/src/Main',
'./styles-new/core.scss',
'./ui/components/layout/Layout.vue',
@ -54,6 +55,7 @@ define([
InspectorViewRegistry,
ToolbarRegistry,
ApplicationRouter,
Browse,
Main,
coreStyles,
Layout,
@ -273,6 +275,7 @@ define([
}.bind(this)
});
// TODO: remove with legacy types.
this.types.listKeys().forEach(function (typeKey) {
var type = this.types.get(typeKey);
var legacyDefinition = type.toLegacyDefinition();
@ -280,26 +283,6 @@ define([
this.legacyExtension('types', legacyDefinition);
}.bind(this));
// TODO: move this to adapter bundle.
this.legacyExtension('runs', {
depends: ['types[]'],
implementation: (types) => {
this.types.importLegacyTypes(types);
}
});
this.objectViews.getAllProviders().forEach(function (p) {
this.legacyExtension('views', {
key: p.key,
provider: p,
name: p.name,
cssClass: p.cssClass,
description: p.description,
editable: p.editable,
template: '<mct-view mct-provider-key="' + p.key + '"/>'
});
}, this);
legacyRegistry.register('adapter', this.legacyBundle);
legacyRegistry.enable('adapter');
@ -324,8 +307,6 @@ define([
// something has depended upon objectService. Cool, right?
this.$injector.get('objectService');
console.log('Rendering app layout.');
var appLayout = new Vue({
mixins: [Layout.default],
provide: {
@ -334,7 +315,8 @@ define([
});
domElement.appendChild(appLayout.$mount().$el);
this.layout = appLayout;
Browse(this);
this.router.start();
this.emit('start');
}.bind(this));

View File

@ -34,7 +34,9 @@ define([
'./runs/TimeSettingsURLHandler',
'./runs/TypeDeprecationChecker',
'./runs/LegacyTelemetryProvider',
'./services/LegacyObjectAPIInterceptor'
'./runs/RegisterLegacyTypes',
'./services/LegacyObjectAPIInterceptor',
'./views/installLegacyViews'
], function (
legacyRegistry,
ActionDialogDecorator,
@ -49,7 +51,9 @@ define([
TimeSettingsURLHandler,
TypeDeprecationChecker,
LegacyTelemetryProvider,
LegacyObjectAPIInterceptor
RegisterLegacyTypes,
LegacyObjectAPIInterceptor,
installLegacyViews
) {
legacyRegistry.register('src/adapter', {
"extensions": {
@ -149,6 +153,21 @@ define([
"openmct",
"instantiate"
]
},
{
implementation: installLegacyViews,
depends: [
"openmct",
"views[]",
"instantiate"
]
},
{
implementation: RegisterLegacyTypes,
depends: [
"types[]",
"openmct"
]
}
],
licenses: [

View File

@ -0,0 +1,17 @@
define([
], function (
) {
function RegisterLegacyTypes(types, openmct) {
types.forEach(function (legacyDefinition) {
if (!openmct.types.get(legacyDefinition.key)) {
console.warn(`DEPRECATION WARNING: Migrate type ${legacyDefinition.key} from ${legacyDefinition.bundle.path} to use the new Types API. Legacy type support will be removed soon.`);
}
});
openmct.types.importLegacyTypes(types);
}
return RegisterLegacyTypes;
});

View File

@ -0,0 +1,93 @@
define([
], function (
) {
function LegacyViewProvider(legacyView, openmct, convertToLegacyObject) {
console.warn(`DEPRECATION WARNING: Migrate ${legacyView.key} from ${legacyView.bundle.path} to use the new View APIs. Legacy view support will be removed soon.`);
return {
key: legacyView.key,
name: legacyView.name,
cssClass: legacyView.cssClass,
description: legacyView.description,
editable: legacyView.editable,
canView: function (domainObject) {
if (!domainObject || !domainObject.identifier) {
return false;
}
if (legacyView.type) {
return domainObject.type === legacyView.type;
}
let legacyObject = convertToLegacyObject(domainObject);
if (legacyView.needs) {
let meetsNeeds = legacyView.needs.every(k => legacyObject.hasCapability(k));
if (!meetsNeeds) {
return false;
}
}
return openmct.$injector.get('policyService').allow(
'view', legacyView, legacyObject
);
},
view: function (domainObject) {
let $rootScope = openmct.$injector.get('$rootScope');
let templateLinker = openmct.$injector.get('templateLinker');
let scope = $rootScope.$new();
let legacyObject = convertToLegacyObject(domainObject);
let isDestroyed = false;
scope.domainObject = legacyObject;
scope.model = legacyObject.getModel();
return {
show: function (container) {
// TODO: implement "gestures" support ?
let uses = legacyView.uses || [];
let promises = [];
let results = uses.map(function (capabilityKey, i) {
let result = legacyObject.useCapability(capabilityKey);
if (result.then) {
promises.push(result.then(function (r) {
results[i] = r;
}));
}
return result;
});
function link() {
if (isDestroyed) {
return;
}
uses.forEach(function (key, i) {
scope[key] = results[i];
});
templateLinker.link(
scope,
openmct.$angular.element(container),
legacyView
);
container.style.height = '100%';
}
if (promises.length) {
Promise.all(promises)
.then(function () {
link();
scope.$digest();
});
} else {
link();
}
},
destroy: function () {
scope.$destroy();
}
}
}
};
};
return LegacyViewProvider;
});

View File

@ -0,0 +1,22 @@
define([
'./LegacyViewProvider',
'../../api/objects/object-utils'
], function (
LegacyViewProvider,
objectUtils
) {
function installLegacyViews(openmct, legacyViews, instantiate) {
function convertToLegacyObject(domainObject) {
let keyString = objectUtils.makeKeyString(domainObject.identifier);
let oldModel = objectUtils.toOldFormat(domainObject);
return instantiate(oldModel, keyString);
}
legacyViews.forEach(function (legacyView) {
openmct.objectViews.addProvider(new LegacyViewProvider(legacyView, openmct, convertToLegacyObject));
});
}
return installLegacyViews;
});

View File

@ -15,7 +15,8 @@ define([
function SummaryWidgetViewProvider(openmct) {
return {
key: 'summary-widget-viewer',
name: 'Widget View',
name: 'Summary View',
cssClass: 'icon-summary-widget',
canView: function (domainObject) {
return domainObject.type === 'summary-widget';
},

View File

@ -1,133 +0,0 @@
<template>
<div class="c-splitter" :class="{
'c-splitter-vertical' : align === 'vertical',
'c-splitter-horizontal' : align === 'horizontal',
'c-splitter-collapse-left' : collapse === 'to-left',
'c-splitter-collapse-right' : collapse === 'to-right',
}">
<a class="c-splitter__btn"></a>
</div>
</template>
<style lang="scss">
@import "~styles/constants";
@import "~styles/constants-snow";
@import "~styles/mixins";
@import "~styles/glyphs";
$c: #06f;
$size: $splitterHandleD;
$margin: 0px;
$hitMargin: 4px;
.c-splitter {
background: $colorSplitterBg;
transition: $transOut;
&:before {
// Bigger hit area
//@include test();
content: '';
display: block;
position: absolute;
z-index: 1;
}
&:active, &:hover {
transition: $transIn;
}
&:active {
background: $colorSplitterActive;
}
&:hover {
background: $colorSplitterHover;
}
&-vertical {
cursor: col-resize;
width: $size;
margin: 0 $margin;
&:before {
top: 0;
right: $hitMargin * -1;
bottom: 0;
left: $hitMargin * -1;
}
}
&-horizontal {
cursor: row-resize;
height: $size;
margin: $margin 0;
&:before {
top: $hitMargin * -1;
right: 0;
bottom: $hitMargin * -1;
left: 0;
}
}
}
.c-splitter__btn {
// Collapse button
background: $colorSplitterBg;
display: none; // Only display if splitter is collapsible, see below
width: $splitterD;
height: 40px;
transition: $transOut;
&:active, &:hover {
transition: $transIn;
}
&:hover {
background: $colorSplitterHover;
}
&:active {
background: $colorSplitterActive;
}
[class*="collapse"] & {
display: flex;
align-items: center;
justify-content: center;
position: absolute;
z-index: 3;
&:before {
color: $colorSplitterFg;
display: block;
font-size: 0.8em;
font-family: symbolsfont;
}
}
[class*="collapse-left"] & {
border-bottom-left-radius: $controlCr;
right: 0;
&:before {
content: $glyph-icon-arrow-left;
}
}
[class*="collapse-right"] & {
border-bottom-right-radius: $controlCr;
left: 0;
&:before {
content: $glyph-icon-arrow-right;
}
}
}
</style>
<script>
export default {
props: {
align: String,
collapse: String
}
}
</script>

View File

@ -2,10 +2,11 @@
<div class="l-browse-bar">
<div class="l-browse-bar__start">
<a class="l-browse-bar__nav-to-parent-button c-icon-button icon-pointer-left"></a>
<div v-bind:class="['l-browse-bar__object-name--w', type.cssClass]">
<span
<div class="l-browse-bar__object-name--w"
:class="type.cssClass">
<span
class="l-browse-bar__object-name c-input-inline"
v-on:blur="updateName"
v-on:blur="updateName"
contenteditable>
{{ domainObject.name }}
</span>
@ -15,19 +16,22 @@
<div class="l-browse-bar__end">
<div class="l-browse-bar__view-switcher c-menu-button--w c-menu-button--menus-left"
v-if="domainObjectViews.length > 1">
<div v-bind:class="['c-menu-button', currentView.cssClass]"
v-if="views.length > 1">
<div class="c-menu-button"
:class="currentView.cssClass"
title="Switch view type"
@click="toggleViewMenu">
<span class="c-button__label">{{ currentView.name }}</span>
<span class="c-button__label">
{{ currentView.name }}
</span>
</div>
<div class="c-menu" v-if="showViewMenu">
<div class="c-menu" v-show="showViewMenu">
<ul>
<li v-for="(view,index) in domainObjectViews"
@click="updateViewParams(view)"
<li v-for="(view, index) in views"
@click="setView(view)"
:key="index"
:class="view.cssClass"
:title="view.title">
:title="view.name">
{{ view.name }}
</li>
</ul>
@ -44,69 +48,64 @@
</template>
<script>
import MenuPlaceholder from '../controls/ContextMenu.vue';
export default {
inject: ['openmct'],
props: {
editNameEnabled: {
type: Boolean,
default: false
}
},
methods: {
toggleViewMenu: function (event) {
event.stopPropagation();
this.showViewMenu = !this.showViewMenu;
},
updateName: function (event) {
// TODO: handle isssues with contenteditable text escaping.
if (event.target.innerText !== this.domainObject.name) {
this.openmct.objects.mutate(this.domainObject, 'name', event.target.innerText);
}
},
updateViewParams: function (view) {
this.openmct.router.updateParams({view: view.key});
},
updateCurrentView: function (viewKey) {
viewKey = viewKey || this.openmct.router.getParams().view;
if (viewKey) {
this.currentView = this.domainObjectViews.find((view) => view.key === viewKey) || {};
}
setView: function (view) {
this.viewKey = view.key;
this.openmct.router.updateParams({
view: this.viewKey
});
}
},
data: function () {
return {
showViewMenu: false,
domainObject: {},
domainObjectViews: [],
currentView: {}
viewKey: undefined
}
},
components: {
MenuPlaceholder
},
computed: {
currentView() {
return this.views.filter(v => v.key === this.viewKey)[0] || {};
},
views() {
return this
.openmct
.objectViews
.get(this.domainObject)
.map((p) => {
return {
key: p.key,
cssClass: p.cssClass,
name: p.name
};
});
},
type() {
return this.openmct.types.get(this.domainObject.type).definition;
let objectType = this.openmct.types.get(this.domainObject.type);
if (!objectType) {
return {}
}
return objectType.definition;
}
},
mounted: function () {
this.$root.$on('main-view-domain-object', (domainObject) => {
this.legacyObject = domainObject;
this.domainObject = domainObject.useCapability('adapter');
this.domainObjectViews = domainObject.useCapability('view');
this.updateCurrentView();
});
document.addEventListener('click', () => {
if (this.showViewMenu) {
this.showViewMenu = false;
}
});
this.openmct.router.on('change:params', (params) => {this.updateCurrentView(params.view)});
}
}
</script>

View File

@ -1,160 +0,0 @@
<template>
<div></div>
</template>
<style lang="scss">
</style>
<script>
// Find an object in an array of objects.
function findObject(domainObjects, id) {
var i;
for (i = 0; i < domainObjects.length; i += 1) {
if (domainObjects[i].getId() === id) {
return domainObjects[i];
}
}
}
// recursively locate and return an object inside of a container
// via a path. If at any point in the recursion it fails to find
// the next object, it will return the parent.
function findViaComposition(containerObject, path) {
var nextId = path.shift();
if (!nextId) {
return containerObject;
}
return containerObject.useCapability('composition')
.then(function (composees) {
var nextObject = findObject(composees, nextId);
if (!nextObject) {
return containerObject;
}
if (!nextObject.hasCapability('composition')) {
return nextObject;
}
return findViaComposition(nextObject, path);
});
}
function getLastChildIfRoot(object) {
if (object.getId() !== 'ROOT') {
return object;
}
return object.useCapability('composition')
.then(function (composees) {
return composees[composees.length - 1];
});
}
function pathForObject(domainObject) {
var context = domainObject.getCapability('context'),
objectPath = context ? context.getPath() : [],
ids = objectPath.map(function (domainObj) {
return domainObj.getId();
});
return "/browse/" + ids.slice(1).join("/");
}
export default {
inject: ["openmct"],
mounted() {
let openmct = this.openmct;
let $injector = openmct.$injector;
let angular = openmct.$angular;
this.objectService = $injector.get('objectService');
this.templateLinker = $injector.get('templateLinker');
this.navigationService = $injector.get('navigationService');
this.$timeout = $injector.get('$timeout');
this.templateMap = {};
$injector.get('templates[]').forEach((t) => {
this.templateMap[t.key] = this.templateMap[t.key] || t;
});
this.$rootScope = $injector.get('$rootScope');
this.$scope = this.$rootScope.$new();
this.$scope.representation = {};
openmct.router.route(/^\/browse\/(.*)$/, (path, results) => {
let navigatePath = results[1];
if (!navigatePath) {
navigatePath = 'mine';
}
this.navigateToPath(navigatePath);
});
this.navigationService.addListener(o => this.navigateToObject(o));
},
destroyed() {
},
methods: {
navigateToPath(path) {
if (!Array.isArray(path)) {
path = path.split('/');
}
return this.getObject('ROOT')
.then(root => {
return findViaComposition(root, path);
})
.then(getLastChildIfRoot)
.then(object => {
this.setMainViewObject(object);
});
},
setMainViewObject(object) {
this.$scope.domainObject = object;
this.$scope.navigatedObject = object;
this.templateLinker.link(
this.$scope,
this.openmct.$angular.element(this.$el),
this.templateMap["browseObject"]
);
document.title = object.getModel().name;
this.$root.$emit('main-view-domain-object', object);
this.scheduleDigest();
},
idsForObject(domainObject) {
return this.urlService
.urlForLocation("", domainObject)
.replace('/', '');
},
navigateToObject(object) {
let path = pathForObject(object);
let views = object.useCapability('view');
let params = this.openmct.router.getParams();
let currentViewIsValid = views.some(v => v.key === params['view']);
if (!currentViewIsValid) {
this.scope.representation = {
selected: views[0]
}
this.openmct.router.update(path, {
view: views[0].key
});
} else {
this.openmct.router.setPath(path);
}
},
scheduleDigest() {
this.$timeout(function () {
// digest done!
});
},
getObject(id) {
return this.objectService.getObjects([id])
.then(function (results) {
return results[id];
});
}
}
}
</script>

View File

@ -22,8 +22,12 @@
</div>
</pane>
<pane class="l-shell__pane-main">
<MainViewBrowseBar class="l-shell__main-view-browse-bar"></MainViewBrowseBar>
<browse-object class="l-shell__main-container"></browse-object>
<browse-bar class="l-shell__main-view-browse-bar"
ref="browseBar">
</browse-bar>
<object-view class="l-shell__main-container"
ref="browseObject">
</object-view>
<mct-template template-key="conductor"
class="l-shell__time-conductor">
</mct-template>
@ -184,28 +188,28 @@
import Inspector from '../inspector/Inspector.vue';
import MctStatus from './MctStatus.vue';
import MctTree from './mct-tree.vue';
import BrowseObject from './BrowseObject.vue';
import ObjectView from './ObjectView.vue';
import MctTemplate from '../legacy/mct-template.vue';
import ContextMenu from '../controls/ContextMenu.vue';
import CreateButton from '../controls/CreateButton.vue';
import search from '../controls/search.vue';
import multipane from '../controls/multipane.vue';
import pane from '../controls/pane.vue';
import MainViewBrowseBar from '../controls/MainViewBrowseBar.vue';
import BrowseBar from './BrowseBar.vue';
export default {
components: {
Inspector,
MctStatus,
MctTree,
BrowseObject,
ObjectView,
'mct-template': MctTemplate,
ContextMenu,
CreateButton,
search,
multipane,
pane,
MainViewBrowseBar
BrowseBar
}
}
</script>

View File

@ -0,0 +1,38 @@
<template>
<div>
<div class="abs l-flex-col ng-scope" ref="mountPoint">
</div>
</div>
</template>
<style lang="scss">
</style>
<script>
export default {
inject: ["openmct"],
destroyed() {
this.clear();
},
methods: {
clear() {
if (this.currentView) {
this.currentView.destroy();
this.$refs.mountPoint.innerHTML = '';
}
delete this.viewContainer;
delete this.currentView;
},
show(object, provider) {
this.clear();
this.currentObject = object;
this.viewContainer = document.createElement('div');
this.$refs.mountPoint.append(this.viewContainer);
this.currentView = provider.view(object);
this.currentView.show(this.viewContainer);
}
}
}
</script>

View File

@ -22,8 +22,6 @@ export default {
let $rootScope = $injector.get('$rootScope');
this.$scope = $rootScope.$new();
console.log('mounting', this.templateKey);
console.log('template:', templateMap[this.templateKey]);
templateLinker.link(
this.$scope,

View File

@ -101,7 +101,7 @@ class ApplicationRouter extends EventEmitter {
doPathChange(newPath, oldPath, newLocation) {
let route = this.routes.filter(r => r.matcher.test(newPath))[0];
if (route) {
route.callback(newPath, route.matcher.exec(newPath));
route.callback(newPath, route.matcher.exec(newPath), this.currentLocation.params);
}
this.emit('change:path', newPath, oldPath);
}

80
src/ui/router/Browse.js Normal file
View File

@ -0,0 +1,80 @@
define([
], function (
) {
return function install(openmct) {
let navigateCall = 0;
let browseObject;
function viewObject(object, viewProvider) {
openmct.layout.$refs.browseObject.show(object, viewProvider);
openmct.layout.$refs.browseBar.domainObject = object;
openmct.layout.$refs.browseBar.viewKey = viewProvider.key;
};
function navigateToPath(path, currentViewKey) {
navigateCall++;
let currentNavigation = navigateCall;
if (!Array.isArray(path)) {
path = path.split('/');
}
let keyString = path[path.length - 1];
// TODO: retain complete path in navigation.
return openmct.objects.get(keyString)
.then((object) => {
if (currentNavigation !== navigateCall) {
return; // Prevent race.
}
openmct.layout.$refs.browseBar.domainObject = object;
browseObject = object;
if (!object) {
openmct.layout.$refs.browseObject.clear();
return;
}
let currentProvider = openmct
.objectViews
.getByProviderKey(currentViewKey)
if (currentProvider && currentProvider.canView(object)) {
viewObject(object, currentProvider);
return;
}
let defaultProvider = openmct.objectViews.get(object)[0];
if (defaultProvider) {
openmct.router.updateParams({
view: defaultProvider.key
});
} else {
openmct.router.updateParams({
view: undefined
});
openmct.layout.$refs.browseObject.clear();
}
});
}
openmct.router.route(/^\/browse\/(.*)$/, (path, results, params) => {
let navigatePath = results[1];
if (!navigatePath) {
navigatePath = 'mine';
}
navigateToPath(navigatePath, params.view);
});
openmct.router.on('change:params', function (newParams, oldParams, changed) {
if (changed.view && browseObject) {
let provider = openmct
.objectViews
.getByProviderKey(changed.view);
viewObject(browseObject, provider);
}
});
}
});