Compare commits
12 Commits
legacy-per
...
fix-image-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
580a055a3c | ||
|
|
6289f34527 | ||
|
|
baa8078d23 | ||
|
|
c112a3052d | ||
|
|
bf0699ceba | ||
|
|
bb62e7953c | ||
|
|
d31ee7c7e6 | ||
|
|
ee60013f45 | ||
|
|
505796d9f0 | ||
|
|
56120ba1bb | ||
|
|
225b235059 | ||
|
|
de614ff606 |
@@ -48,6 +48,7 @@
|
||||
openmct.install(openmct.plugins.MyItems());
|
||||
openmct.install(openmct.plugins.Generator());
|
||||
openmct.install(openmct.plugins.ExampleImagery());
|
||||
openmct.install(openmct.plugins.Timeline());
|
||||
openmct.install(openmct.plugins.UTCTimeSystem());
|
||||
openmct.install(openmct.plugins.AutoflowView({
|
||||
type: "telemetry.panel"
|
||||
|
||||
@@ -86,7 +86,7 @@ module.exports = (config) => {
|
||||
reports: ['html', 'lcovonly', 'text-summary'],
|
||||
thresholds: {
|
||||
global: {
|
||||
lines: 64
|
||||
lines: 65
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -30,7 +30,6 @@ define([
|
||||
"./src/controllers/CompositeController",
|
||||
"./src/controllers/ColorController",
|
||||
"./src/controllers/DialogButtonController",
|
||||
"./src/controllers/SnapshotPreviewController",
|
||||
"./res/templates/controls/autocomplete.html",
|
||||
"./res/templates/controls/checkbox.html",
|
||||
"./res/templates/controls/datetime.html",
|
||||
@@ -44,8 +43,7 @@ define([
|
||||
"./res/templates/controls/menu-button.html",
|
||||
"./res/templates/controls/dialog.html",
|
||||
"./res/templates/controls/radio.html",
|
||||
"./res/templates/controls/file-input.html",
|
||||
"./res/templates/controls/snap-view.html"
|
||||
"./res/templates/controls/file-input.html"
|
||||
], function (
|
||||
MCTForm,
|
||||
MCTControl,
|
||||
@@ -56,7 +54,6 @@ define([
|
||||
CompositeController,
|
||||
ColorController,
|
||||
DialogButtonController,
|
||||
SnapshotPreviewController,
|
||||
autocompleteTemplate,
|
||||
checkboxTemplate,
|
||||
datetimeTemplate,
|
||||
@@ -70,8 +67,7 @@ define([
|
||||
menuButtonTemplate,
|
||||
dialogTemplate,
|
||||
radioTemplate,
|
||||
fileInputTemplate,
|
||||
snapViewTemplate
|
||||
fileInputTemplate
|
||||
) {
|
||||
|
||||
return {
|
||||
@@ -157,10 +153,6 @@ define([
|
||||
{
|
||||
"key": "file-input",
|
||||
"template": fileInputTemplate
|
||||
},
|
||||
{
|
||||
"key": "snap-view",
|
||||
"template": snapViewTemplate
|
||||
}
|
||||
],
|
||||
"controllers": [
|
||||
@@ -194,14 +186,6 @@ define([
|
||||
"$scope",
|
||||
"dialogService"
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "SnapshotPreviewController",
|
||||
"implementation": SnapshotPreviewController,
|
||||
"depends": [
|
||||
"$scope",
|
||||
"openmct"
|
||||
]
|
||||
}
|
||||
],
|
||||
"components": [
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
<!--
|
||||
Open MCT, Copyright (c) 2014-2020, United States Government
|
||||
as represented by the Administrator of the National Aeronautics and Space
|
||||
Administration. All rights reserved.
|
||||
|
||||
Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
"License"); you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
http://www.apache.org/licenses/LICENSE-2.0.
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
License for the specific language governing permissions and limitations
|
||||
under the License.
|
||||
|
||||
Open MCT includes source code licensed under additional open source
|
||||
licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
this source code distribution or the Licensing information page available
|
||||
at runtime from the About dialog for additional information.
|
||||
-->
|
||||
<span ng-controller="SnapshotPreviewController"
|
||||
class='form-control shell'>
|
||||
<span class='field control {{structure.cssClass}}'>
|
||||
<image
|
||||
class="c-ne__embed__snap-thumb"
|
||||
src="{{imageUrl || structure.src}}"
|
||||
ng-click="previewImage(imageUrl || structure.src)"
|
||||
name="mctControl">
|
||||
</image>
|
||||
<br>
|
||||
<a title="Annotate" class="s-button icon-pencil" ng-click="annotateImage(ngModel, field, imageUrl || structure.src)">
|
||||
<span class="title-label">Annotate</span>
|
||||
</a>
|
||||
</span>
|
||||
</span>
|
||||
@@ -29,7 +29,6 @@ define(["zepto"], function ($) {
|
||||
* @memberof platform/forms
|
||||
*/
|
||||
function FileInputService() {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -38,7 +37,7 @@ define(["zepto"], function ($) {
|
||||
*
|
||||
* @returns {Promise} promise for an object containing file meta-data
|
||||
*/
|
||||
FileInputService.prototype.getInput = function () {
|
||||
FileInputService.prototype.getInput = function (fileType) {
|
||||
var input = this.newInput();
|
||||
var read = this.readFile;
|
||||
var fileInfo = {};
|
||||
@@ -51,6 +50,10 @@ define(["zepto"], function ($) {
|
||||
file = this.files[0];
|
||||
input.remove();
|
||||
if (file) {
|
||||
if (fileType && (!file.type || (file.type !== fileType))) {
|
||||
reject("Incompatible file type");
|
||||
}
|
||||
|
||||
read(file)
|
||||
.then(function (contents) {
|
||||
fileInfo.name = file.name;
|
||||
|
||||
@@ -40,7 +40,7 @@ define(
|
||||
}
|
||||
|
||||
function handleClick() {
|
||||
fileInputService.getInput().then(function (result) {
|
||||
fileInputService.getInput(scope.structure.type).then(function (result) {
|
||||
setText(result.name);
|
||||
scope.ngModel[scope.field] = result;
|
||||
control.$setValidity("file-input", true);
|
||||
|
||||
@@ -1,132 +0,0 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2020, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
define(
|
||||
[
|
||||
'painterro'
|
||||
],
|
||||
function (Painterro) {
|
||||
|
||||
function SnapshotPreviewController($scope, openmct) {
|
||||
|
||||
$scope.previewImage = function (imageUrl) {
|
||||
let imageDiv = document.createElement('div');
|
||||
imageDiv.classList = 'image-main s-image-main';
|
||||
imageDiv.style.backgroundImage = `url(${imageUrl})`;
|
||||
|
||||
let previewImageOverlay = openmct.overlays.overlay(
|
||||
{
|
||||
element: imageDiv,
|
||||
size: 'large',
|
||||
buttons: [
|
||||
{
|
||||
label: 'Done',
|
||||
callback: function () {
|
||||
previewImageOverlay.dismiss();
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
$scope.annotateImage = function (ngModel, field, imageUrl) {
|
||||
$scope.imageUrl = imageUrl;
|
||||
|
||||
let div = document.createElement('div'),
|
||||
painterroInstance = {},
|
||||
save = false;
|
||||
|
||||
div.id = 'snap-annotation';
|
||||
|
||||
let annotateImageOverlay = openmct.overlays.overlay(
|
||||
{
|
||||
element: div,
|
||||
size: 'large',
|
||||
buttons: [
|
||||
{
|
||||
label: 'Cancel',
|
||||
callback: function () {
|
||||
save = false;
|
||||
painterroInstance.save();
|
||||
annotateImageOverlay.dismiss();
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'Save',
|
||||
callback: function () {
|
||||
save = true;
|
||||
painterroInstance.save();
|
||||
annotateImageOverlay.dismiss();
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
);
|
||||
|
||||
painterroInstance = Painterro({
|
||||
id: 'snap-annotation',
|
||||
activeColor: '#ff0000',
|
||||
activeColorAlpha: 1.0,
|
||||
activeFillColor: '#fff',
|
||||
activeFillColorAlpha: 0.0,
|
||||
backgroundFillColor: '#000',
|
||||
backgroundFillColorAlpha: 0.0,
|
||||
defaultFontSize: 16,
|
||||
defaultLineWidth: 2,
|
||||
defaultTool: 'ellipse',
|
||||
hiddenTools: ['save', 'open', 'close', 'eraser', 'pixelize', 'rotate', 'settings', 'resize'],
|
||||
translation: {
|
||||
name: 'en',
|
||||
strings: {
|
||||
lineColor: 'Line',
|
||||
fillColor: 'Fill',
|
||||
lineWidth: 'Size',
|
||||
textColor: 'Color',
|
||||
fontSize: 'Size',
|
||||
fontStyle: 'Style'
|
||||
}
|
||||
},
|
||||
saveHandler: function (image, done) {
|
||||
if (save) {
|
||||
let url = image.asBlob(),
|
||||
reader = new window.FileReader();
|
||||
|
||||
reader.readAsDataURL(url);
|
||||
reader.onloadend = function () {
|
||||
$scope.imageUrl = reader.result;
|
||||
ngModel[field] = reader.result;
|
||||
};
|
||||
} else {
|
||||
ngModel.field = imageUrl;
|
||||
console.warn('You cancelled the annotation!!!');
|
||||
}
|
||||
|
||||
done(true);
|
||||
}
|
||||
}).show(imageUrl);
|
||||
};
|
||||
}
|
||||
|
||||
return SnapshotPreviewController;
|
||||
}
|
||||
);
|
||||
@@ -104,7 +104,7 @@ define([
|
||||
"depends": [
|
||||
"$q",
|
||||
"$log",
|
||||
"modelService",
|
||||
"objectService",
|
||||
"workerService",
|
||||
"topic",
|
||||
"GENERIC_SEARCH_ROOTS",
|
||||
|
||||
@@ -38,16 +38,16 @@ define([
|
||||
* @constructor
|
||||
* @param $q Angular's $q, for promise consolidation.
|
||||
* @param $log Anglar's $log, for logging.
|
||||
* @param {ModelService} modelService the model service.
|
||||
* @param {ObjectService} objectService the object service.
|
||||
* @param {WorkerService} workerService the workerService.
|
||||
* @param {TopicService} topic the topic service.
|
||||
* @param {Array} ROOTS An array of object Ids to begin indexing.
|
||||
*/
|
||||
function GenericSearchProvider($q, $log, modelService, workerService, topic, ROOTS, USE_LEGACY_INDEXER, openmct) {
|
||||
function GenericSearchProvider($q, $log, objectService, workerService, topic, ROOTS, USE_LEGACY_INDEXER, openmct) {
|
||||
var provider = this;
|
||||
this.$q = $q;
|
||||
this.$log = $log;
|
||||
this.modelService = modelService;
|
||||
this.objectService = objectService;
|
||||
this.openmct = openmct;
|
||||
|
||||
this.indexedIds = {};
|
||||
@@ -218,12 +218,12 @@ define([
|
||||
provider = this;
|
||||
|
||||
this.pendingRequests += 1;
|
||||
this.modelService
|
||||
.getModels([idToIndex])
|
||||
.then(function (models) {
|
||||
this.objectService
|
||||
.getObjects([idToIndex])
|
||||
.then(function (objects) {
|
||||
delete provider.pendingIndex[idToIndex];
|
||||
if (models[idToIndex]) {
|
||||
provider.index(idToIndex, models[idToIndex]);
|
||||
if (objects[idToIndex]) {
|
||||
provider.index(idToIndex, objects[idToIndex].model);
|
||||
}
|
||||
}, function () {
|
||||
provider
|
||||
|
||||
@@ -20,8 +20,6 @@
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
import objectUtils from 'objectUtils';
|
||||
|
||||
export default class LegacyPersistenceAdapter {
|
||||
constructor(openmct) {
|
||||
this.openmct = openmct;
|
||||
@@ -39,9 +37,16 @@ export default class LegacyPersistenceAdapter {
|
||||
return this.openmct.objects.save(legacyDomainObject.useCapability('adapter'));
|
||||
}
|
||||
|
||||
readObject(keystring) {
|
||||
let identifier = objectUtils.parseKeyString(keystring);
|
||||
readObject(space, key) {
|
||||
const identifier = {
|
||||
namespace: space,
|
||||
key: key
|
||||
};
|
||||
|
||||
return this.openmct.legacyObject(this.openmct.objects.get(identifier));
|
||||
return this.openmct.objects.get(identifier).then(domainObject => {
|
||||
let object = this.openmct.legacyObject(domainObject);
|
||||
|
||||
return object.model;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -153,9 +153,12 @@ export default {
|
||||
: this.imageUrl;
|
||||
},
|
||||
getTime(datum) {
|
||||
return datum
|
||||
let dateTimeStr = datum
|
||||
? this.timeFormat.format(datum)
|
||||
: this.time;
|
||||
|
||||
// Replace ISO "T" with a space to allow wrapping
|
||||
return dateTimeStr ? dateTimeStr.replace("T", " ") : "";
|
||||
},
|
||||
handleScroll() {
|
||||
const thumbsWrapper = this.$refs.thumbsWrapper;
|
||||
|
||||
@@ -9,10 +9,11 @@
|
||||
</div>
|
||||
<SearchResults v-if="search.length"
|
||||
ref="searchResults"
|
||||
:results="getSearchResults()"
|
||||
:domain-object="internalDomainObject"
|
||||
:results="searchedEntries"
|
||||
@changeSectionPage="changeSelectedSection"
|
||||
@updateEntries="updateEntries"
|
||||
/>
|
||||
|
||||
<div v-if="!search.length"
|
||||
class="c-notebook__body"
|
||||
>
|
||||
@@ -105,10 +106,10 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import NotebookEntry from './notebook-entry.vue';
|
||||
import NotebookEntry from './NotebookEntry.vue';
|
||||
import Search from '@/ui/components/search.vue';
|
||||
import SearchResults from './search-results.vue';
|
||||
import Sidebar from './sidebar.vue';
|
||||
import SearchResults from './SearchResults.vue';
|
||||
import Sidebar from './Sidebar.vue';
|
||||
import { clearDefaultNotebook, getDefaultNotebook, setDefaultNotebook, setDefaultNotebookSection, setDefaultNotebookPage } from '../utils/notebook-storage';
|
||||
import { addNotebookEntry, createNewEmbed, getNotebookEntries } from '../utils/notebook-entries';
|
||||
import { throttle } from 'lodash';
|
||||
@@ -153,6 +154,9 @@ export default {
|
||||
pages() {
|
||||
return this.getPages() || [];
|
||||
},
|
||||
searchedEntries() {
|
||||
return this.getSearchResults();
|
||||
},
|
||||
sections() {
|
||||
return this.internalDomainObject.configuration.sections || [];
|
||||
},
|
||||
@@ -172,8 +176,6 @@ export default {
|
||||
return this.sections.find(section => section.isSelected);
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
},
|
||||
beforeMount() {
|
||||
this.throttledSearchItem = throttle(this.searchItem, 500);
|
||||
},
|
||||
@@ -259,7 +261,7 @@ export default {
|
||||
event.preventDefault();
|
||||
event.stopImmediatePropagation();
|
||||
|
||||
const snapshotId = event.dataTransfer.getData('snapshot/id');
|
||||
const snapshotId = event.dataTransfer.getData('openmct/snapshot/id');
|
||||
if (snapshotId.length) {
|
||||
const snapshot = this.snapshotContainer.getSnapshot(snapshotId);
|
||||
this.newEntry(snapshot);
|
||||
@@ -17,7 +17,7 @@
|
||||
<div v-if="embed.snapshot"
|
||||
class="c-ne__embed__time"
|
||||
>
|
||||
{{ formatTime(embed.createdOn, 'YYYY-MM-DD HH:mm:ss') }}
|
||||
{{ createdOn }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -25,10 +25,10 @@
|
||||
|
||||
<script>
|
||||
import Moment from 'moment';
|
||||
import PopupMenu from './popup-menu.vue';
|
||||
import PopupMenu from './PopupMenu.vue';
|
||||
import PreviewAction from '../../../ui/preview/PreviewAction';
|
||||
import Painterro from 'painterro';
|
||||
import RemoveDialog from '../utils/removeDialog';
|
||||
import PainterroInstance from '../utils/painterroInstance';
|
||||
import SnapshotTemplate from './snapshot-template.html';
|
||||
import Vue from 'vue';
|
||||
|
||||
@@ -56,7 +56,10 @@ export default {
|
||||
popupMenuItems: []
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
computed: {
|
||||
createdOn() {
|
||||
return this.formatTime(this.embed.createdOn, 'YYYY-MM-DD HH:mm:ss');
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.addPopupMenuItems();
|
||||
@@ -78,95 +81,44 @@ export default {
|
||||
this.popupMenuItems = [removeEmbed, preview];
|
||||
},
|
||||
annotateSnapshot() {
|
||||
const self = this;
|
||||
|
||||
let save = false;
|
||||
let painterroInstance = {};
|
||||
const annotateVue = new Vue({
|
||||
template: '<div id="snap-annotation"></div>'
|
||||
});
|
||||
}).$mount();
|
||||
|
||||
let annotateOverlay = self.openmct.overlays.overlay({
|
||||
element: annotateVue.$mount().$el,
|
||||
const painterroInstance = new PainterroInstance(annotateVue.$el, this.updateSnapshot);
|
||||
const annotateOverlay = this.openmct.overlays.overlay({
|
||||
element: annotateVue.$el,
|
||||
size: 'large',
|
||||
dismissable: false,
|
||||
buttons: [
|
||||
{
|
||||
label: 'Cancel',
|
||||
callback: function () {
|
||||
save = false;
|
||||
painterroInstance.save();
|
||||
emphasis: true,
|
||||
callback: () => {
|
||||
painterroInstance.dismiss();
|
||||
annotateOverlay.dismiss();
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'Save',
|
||||
callback: function () {
|
||||
|
||||
save = true;
|
||||
callback: () => {
|
||||
painterroInstance.save();
|
||||
annotateOverlay.dismiss();
|
||||
this.snapshotOverlay.dismiss();
|
||||
this.openSnapshot();
|
||||
}
|
||||
}
|
||||
],
|
||||
onDestroy: function () {
|
||||
onDestroy: () => {
|
||||
annotateVue.$destroy(true);
|
||||
}
|
||||
});
|
||||
|
||||
painterroInstance = Painterro({
|
||||
id: 'snap-annotation',
|
||||
activeColor: '#ff0000',
|
||||
activeColorAlpha: 1.0,
|
||||
activeFillColor: '#fff',
|
||||
activeFillColorAlpha: 0.0,
|
||||
backgroundFillColor: '#000',
|
||||
backgroundFillColorAlpha: 0.0,
|
||||
defaultFontSize: 16,
|
||||
defaultLineWidth: 2,
|
||||
defaultTool: 'ellipse',
|
||||
hiddenTools: ['save', 'open', 'close', 'eraser', 'pixelize', 'rotate', 'settings', 'resize'],
|
||||
translation: {
|
||||
name: 'en',
|
||||
strings: {
|
||||
lineColor: 'Line',
|
||||
fillColor: 'Fill',
|
||||
lineWidth: 'Size',
|
||||
textColor: 'Color',
|
||||
fontSize: 'Size',
|
||||
fontStyle: 'Style'
|
||||
}
|
||||
},
|
||||
saveHandler: function (image, done) {
|
||||
if (save) {
|
||||
const url = image.asBlob();
|
||||
const reader = new window.FileReader();
|
||||
reader.readAsDataURL(url);
|
||||
reader.onloadend = function () {
|
||||
const snapshot = reader.result;
|
||||
const snapshotObject = {
|
||||
src: snapshot,
|
||||
type: url.type,
|
||||
size: url.size,
|
||||
modified: Date.now()
|
||||
};
|
||||
|
||||
self.embed.snapshot = snapshotObject;
|
||||
self.updateEmbed(self.embed);
|
||||
};
|
||||
} else {
|
||||
console.log('You cancelled the annotation!!!');
|
||||
}
|
||||
|
||||
done(true);
|
||||
}
|
||||
}).show(this.embed.snapshot.src);
|
||||
painterroInstance.intialize();
|
||||
painterroInstance.show(this.embed.snapshot.src);
|
||||
},
|
||||
changeLocation() {
|
||||
const link = this.embed.historicLink;
|
||||
if (!link) {
|
||||
return;
|
||||
}
|
||||
|
||||
const bounds = this.openmct.time.bounds();
|
||||
const isTimeBoundChanged = this.embed.bounds.start !== bounds.start
|
||||
@@ -209,7 +161,8 @@ export default {
|
||||
this.snapshot = new Vue({
|
||||
data: () => {
|
||||
return {
|
||||
embed: self.embed
|
||||
createdOn: this.createdOn,
|
||||
embed: this.embed
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
@@ -218,13 +171,11 @@ export default {
|
||||
exportImage: self.exportImage
|
||||
},
|
||||
template: SnapshotTemplate
|
||||
});
|
||||
}).$mount();
|
||||
|
||||
const snapshotOverlay = this.openmct.overlays.overlay({
|
||||
element: this.snapshot.$mount().$el,
|
||||
onDestroy: () => {
|
||||
this.snapshot.$destroy(true);
|
||||
},
|
||||
this.snapshotOverlay = this.openmct.overlays.overlay({
|
||||
element: this.snapshot.$el,
|
||||
onDestroy: () => this.snapshot.$destroy(true),
|
||||
size: 'large',
|
||||
dismissable: true,
|
||||
buttons: [
|
||||
@@ -232,7 +183,7 @@ export default {
|
||||
label: 'Done',
|
||||
emphasis: true,
|
||||
callback: () => {
|
||||
snapshotOverlay.dismiss();
|
||||
this.snapshotOverlay.dismiss();
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -262,6 +213,10 @@ export default {
|
||||
},
|
||||
updateEmbed(embed) {
|
||||
this.$emit('updateEmbed', embed);
|
||||
},
|
||||
updateSnapshot(snapshotObject) {
|
||||
this.embed.snapshot = snapshotObject;
|
||||
this.updateEmbed(this.embed);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -1,13 +1,13 @@
|
||||
<template>
|
||||
<div class="c-notebook__entry c-ne has-local-controls"
|
||||
@dragover="dragover"
|
||||
@drop.capture="dropCapture"
|
||||
@drop.prevent="dropOnEntry(entry.id, $event)"
|
||||
@dragover="changeCursor"
|
||||
@drop.capture="cancelEditMode"
|
||||
@drop.prevent="dropOnEntry"
|
||||
>
|
||||
<div class="c-ne__time-and-content">
|
||||
<div class="c-ne__time">
|
||||
<span>{{ formatTime(entry.createdOn, 'YYYY-MM-DD') }}</span>
|
||||
<span>{{ formatTime(entry.createdOn, 'HH:mm:ss') }}</span>
|
||||
<span>{{ createdOnDate }}</span>
|
||||
<span>{{ createdOnTime }}</span>
|
||||
</div>
|
||||
<div class="c-ne__content">
|
||||
<div :id="entry.id"
|
||||
@@ -15,8 +15,8 @@
|
||||
:class="{'c-input-inline' : !readOnly }"
|
||||
:contenteditable="!readOnly"
|
||||
:style="!entry.text.length ? defaultEntryStyle : ''"
|
||||
@blur="textBlur($event, entry.id)"
|
||||
@focus="textFocus($event, entry.id)"
|
||||
@blur="updateEntryValue($event, entry.id)"
|
||||
@focus="updateCurrentEntryValue($event, entry.id)"
|
||||
>{{ entry.text.length ? entry.text : defaultText }}</div>
|
||||
<div class="c-snapshots c-ne__embeds">
|
||||
<NotebookEmbed v-for="embed in entry.embeds"
|
||||
@@ -57,7 +57,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import NotebookEmbed from './notebook-embed.vue';
|
||||
import NotebookEmbed from './NotebookEmbed.vue';
|
||||
import { createNewEmbed, getEntryPosById, getNotebookEntries } from '../utils/notebook-entries';
|
||||
import Moment from 'moment';
|
||||
|
||||
@@ -114,29 +114,32 @@ export default {
|
||||
defaultText: 'add description'
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
entry() {
|
||||
computed: {
|
||||
createdOnDate() {
|
||||
return this.formatTime(this.entry.createdOn, 'YYYY-MM-DD');
|
||||
},
|
||||
readOnly(readOnly) {
|
||||
},
|
||||
selectedSection(selectedSection) {
|
||||
},
|
||||
selectedPage(selectedSection) {
|
||||
createdOnTime() {
|
||||
return this.formatTime(this.entry.createdOn, 'HH:mm:ss');
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.updateEntries = this.updateEntries.bind(this);
|
||||
},
|
||||
beforeDestory() {
|
||||
this.dropOnEntry = this.dropOnEntry.bind(this);
|
||||
},
|
||||
methods: {
|
||||
cancelEditMode(event) {
|
||||
const isEditing = this.openmct.editor.isEditing();
|
||||
if (isEditing) {
|
||||
this.openmct.editor.cancel();
|
||||
}
|
||||
},
|
||||
changeCursor() {
|
||||
event.preventDefault();
|
||||
event.dataTransfer.dropEffect = "copy";
|
||||
},
|
||||
deleteEntry() {
|
||||
const self = this;
|
||||
if (!self.domainObject || !self.selectedSection || !self.selectedPage || !self.entry.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
const entryPosById = this.entryPosById(this.entry.id);
|
||||
const entryPosById = self.entryPosById(self.entry.id);
|
||||
if (entryPosById === -1) {
|
||||
return;
|
||||
}
|
||||
@@ -151,7 +154,7 @@ export default {
|
||||
callback: () => {
|
||||
const entries = getNotebookEntries(self.domainObject, self.selectedSection, self.selectedPage);
|
||||
entries.splice(entryPosById, 1);
|
||||
this.updateEntries(entries);
|
||||
self.updateEntries(entries);
|
||||
dialog.dismiss();
|
||||
}
|
||||
},
|
||||
@@ -164,24 +167,10 @@ export default {
|
||||
]
|
||||
});
|
||||
},
|
||||
dragover() {
|
||||
event.preventDefault();
|
||||
event.dataTransfer.dropEffect = "copy";
|
||||
},
|
||||
dropCapture(event) {
|
||||
const isEditing = this.openmct.editor.isEditing();
|
||||
if (isEditing) {
|
||||
this.openmct.editor.cancel();
|
||||
}
|
||||
},
|
||||
dropOnEntry(entryId, $event) {
|
||||
dropOnEntry($event) {
|
||||
event.stopImmediatePropagation();
|
||||
|
||||
if (!this.domainObject || !this.selectedSection || !this.selectedPage) {
|
||||
return;
|
||||
}
|
||||
|
||||
const snapshotId = $event.dataTransfer.getData('snapshot/id');
|
||||
const snapshotId = $event.dataTransfer.getData('openmct/snapshot/id');
|
||||
if (snapshotId.length) {
|
||||
this.moveSnapshot(snapshotId);
|
||||
|
||||
@@ -190,7 +179,7 @@ export default {
|
||||
|
||||
const data = $event.dataTransfer.getData('openmct/domain-object-path');
|
||||
const objectPath = JSON.parse(data);
|
||||
const entryPos = this.entryPosById(entryId);
|
||||
const entryPos = this.entryPosById(this.entry.id);
|
||||
const bounds = this.openmct.time.bounds();
|
||||
const snapshotMeta = {
|
||||
bounds,
|
||||
@@ -253,7 +242,44 @@ export default {
|
||||
selection.removeAllRanges();
|
||||
selection.addRange(range);
|
||||
},
|
||||
textBlur($event, entryId) {
|
||||
updateCurrentEntryValue($event) {
|
||||
if (this.readOnly) {
|
||||
return;
|
||||
}
|
||||
|
||||
const target = $event.target;
|
||||
this.currentEntryValue = target ? target.innerText : '';
|
||||
|
||||
if (!this.entry.text.length) {
|
||||
this.selectTextInsideElement(target);
|
||||
}
|
||||
},
|
||||
updateEmbed(newEmbed) {
|
||||
this.entry.embeds.some(e => {
|
||||
const found = (e.id === newEmbed.id);
|
||||
if (found) {
|
||||
e = newEmbed;
|
||||
}
|
||||
|
||||
return found;
|
||||
});
|
||||
|
||||
this.updateEntry(this.entry);
|
||||
},
|
||||
updateEntry(newEntry) {
|
||||
const entries = getNotebookEntries(this.domainObject, this.selectedSection, this.selectedPage);
|
||||
entries.some(entry => {
|
||||
const found = (entry.id === newEntry.id);
|
||||
if (found) {
|
||||
entry = newEntry;
|
||||
}
|
||||
|
||||
return found;
|
||||
});
|
||||
|
||||
this.updateEntries(entries);
|
||||
},
|
||||
updateEntryValue($event, entryId) {
|
||||
if (!this.domainObject || !this.selectedSection || !this.selectedPage) {
|
||||
return;
|
||||
}
|
||||
@@ -272,42 +298,6 @@ export default {
|
||||
this.updateEntries(entries);
|
||||
}
|
||||
},
|
||||
textFocus($event) {
|
||||
if (this.readOnly || !this.domainObject || !this.selectedSection || !this.selectedPage) {
|
||||
return;
|
||||
}
|
||||
|
||||
const target = $event.target;
|
||||
this.currentEntryValue = target ? target.innerText : '';
|
||||
|
||||
if (!this.entry.text.length) {
|
||||
this.selectTextInsideElement(target);
|
||||
}
|
||||
},
|
||||
updateEmbed(newEmbed) {
|
||||
let embed = this.entry.embeds.find(e => e.id === newEmbed.id);
|
||||
|
||||
if (!embed) {
|
||||
return;
|
||||
}
|
||||
|
||||
embed = newEmbed;
|
||||
this.updateEntry(this.entry);
|
||||
},
|
||||
updateEntry(newEntry) {
|
||||
if (!this.domainObject || !this.selectedSection || !this.selectedPage) {
|
||||
return;
|
||||
}
|
||||
|
||||
const entries = getNotebookEntries(this.domainObject, this.selectedSection, this.selectedPage);
|
||||
entries.forEach(entry => {
|
||||
if (entry.id === newEntry.id) {
|
||||
entry = newEntry;
|
||||
}
|
||||
});
|
||||
|
||||
this.updateEntries(entries);
|
||||
},
|
||||
updateEntries(entries) {
|
||||
this.$emit('updateEntries', entries);
|
||||
}
|
||||
@@ -49,8 +49,8 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import NotebookEmbed from './notebook-embed.vue';
|
||||
import PopupMenu from './popup-menu.vue';
|
||||
import NotebookEmbed from './NotebookEmbed.vue';
|
||||
import PopupMenu from './PopupMenu.vue';
|
||||
import RemoveDialog from '../utils/removeDialog';
|
||||
import { NOTEBOOK_SNAPSHOT_MAX_COUNT } from '../snapshot-container';
|
||||
import { EVENT_SNAPSHOTS_UPDATED } from '../notebook-constants';
|
||||
@@ -81,8 +81,6 @@ export default {
|
||||
this.snapshotContainer.on(EVENT_SNAPSHOTS_UPDATED, this.snapshotsUpdated);
|
||||
this.snapshots = this.snapshotContainer.getSnapshots();
|
||||
},
|
||||
beforeDestory() {
|
||||
},
|
||||
methods: {
|
||||
addPopupMenuItems() {
|
||||
const removeSnapshot = {
|
||||
@@ -122,7 +120,7 @@ export default {
|
||||
},
|
||||
startEmbedDrag(snapshot, event) {
|
||||
event.dataTransfer.setData('text/plain', snapshot.id);
|
||||
event.dataTransfer.setData('snapshot/id', snapshot.id);
|
||||
event.dataTransfer.setData('openmct/snapshot/id', snapshot.id);
|
||||
},
|
||||
updateSnapshot(snapshot) {
|
||||
this.snapshotContainer.updateSnapshot(snapshot);
|
||||
@@ -18,7 +18,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import SnapshotContainerComponent from './notebook-snapshot-container.vue';
|
||||
import SnapshotContainerComponent from './NotebookSnapshotContainer.vue';
|
||||
import { EVENT_SNAPSHOTS_UPDATED } from '../notebook-constants';
|
||||
import { NOTEBOOK_SNAPSHOT_MAX_COUNT } from '../snapshot-container';
|
||||
import Vue from 'vue';
|
||||
@@ -19,7 +19,7 @@
|
||||
<script>
|
||||
import { deleteNotebookEntries } from '../utils/notebook-entries';
|
||||
import { getDefaultNotebook } from '../utils/notebook-storage';
|
||||
import Page from './page-component.vue';
|
||||
import Page from './PageComponent.vue';
|
||||
|
||||
export default {
|
||||
inject: ['openmct'],
|
||||
@@ -70,12 +70,6 @@ export default {
|
||||
return {
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
},
|
||||
mounted() {
|
||||
},
|
||||
destroyed() {
|
||||
},
|
||||
methods: {
|
||||
deletePage(id) {
|
||||
const selectedSection = this.sections.find(s => s.isSelected);
|
||||
@@ -14,7 +14,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import PopupMenu from './popup-menu.vue';
|
||||
import PopupMenu from './PopupMenu.vue';
|
||||
import RemoveDialog from '../utils/removeDialog';
|
||||
|
||||
export default {
|
||||
@@ -55,8 +55,6 @@ export default {
|
||||
this.addPopupMenuItems();
|
||||
this.toggleContentEditable();
|
||||
},
|
||||
destroyed() {
|
||||
},
|
||||
methods: {
|
||||
addPopupMenuItems() {
|
||||
const removePage = {
|
||||
@@ -8,7 +8,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import MenuItems from './menu-items.vue';
|
||||
import MenuItems from './MenuItems.vue';
|
||||
import Vue from 'vue';
|
||||
|
||||
export default {
|
||||
@@ -4,26 +4,34 @@
|
||||
<div class="c-notebook__entries">
|
||||
<NotebookEntry v-for="(result, index) in results"
|
||||
:key="index"
|
||||
:domain-object="domainObject"
|
||||
:result="result"
|
||||
:entry="result.entry"
|
||||
:read-only="true"
|
||||
:selected-page="null"
|
||||
:selected-section="null"
|
||||
:selected-page="result.page"
|
||||
:selected-section="result.section"
|
||||
@changeSectionPage="changeSectionPage"
|
||||
@updateEntries="updateEntries"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import NotebookEntry from './notebook-entry.vue';
|
||||
import NotebookEntry from './NotebookEntry.vue';
|
||||
|
||||
export default {
|
||||
inject: ['openmct', 'domainObject'],
|
||||
inject: ['openmct', 'snapshotContainer'],
|
||||
components: {
|
||||
NotebookEntry
|
||||
},
|
||||
props: {
|
||||
domainObject: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {};
|
||||
}
|
||||
},
|
||||
results: {
|
||||
type: Array,
|
||||
default() {
|
||||
@@ -31,19 +39,12 @@ export default {
|
||||
}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {};
|
||||
},
|
||||
watch: {
|
||||
results(newResults) {}
|
||||
},
|
||||
destroyed() {
|
||||
},
|
||||
mounted() {
|
||||
},
|
||||
methods: {
|
||||
changeSectionPage(data) {
|
||||
this.$emit('changeSectionPage', data);
|
||||
},
|
||||
updateEntries(entries) {
|
||||
this.$emit('updateEntries', entries);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -19,7 +19,7 @@
|
||||
<script>
|
||||
import { deleteNotebookEntries } from '../utils/notebook-entries';
|
||||
import { getDefaultNotebook } from '../utils/notebook-storage';
|
||||
import sectionComponent from './section-component.vue';
|
||||
import sectionComponent from './SectionComponent.vue';
|
||||
|
||||
export default {
|
||||
inject: ['openmct'],
|
||||
@@ -57,12 +57,6 @@ export default {
|
||||
return {
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
},
|
||||
mounted() {
|
||||
},
|
||||
destroyed() {
|
||||
},
|
||||
methods: {
|
||||
deleteSection(id) {
|
||||
const section = this.sections.find(s => s.id === id);
|
||||
@@ -17,7 +17,7 @@
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import PopupMenu from './popup-menu.vue';
|
||||
import PopupMenu from './PopupMenu.vue';
|
||||
import RemoveDialog from '../utils/removeDialog';
|
||||
|
||||
export default {
|
||||
@@ -58,8 +58,6 @@ export default {
|
||||
this.addPopupMenuItems();
|
||||
this.toggleContentEditable();
|
||||
},
|
||||
destroyed() {
|
||||
},
|
||||
methods: {
|
||||
addPopupMenuItems() {
|
||||
const removeSection = {
|
||||
@@ -56,8 +56,8 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import SectionCollection from './section-collection.vue';
|
||||
import PageCollection from './page-collection.vue';
|
||||
import SectionCollection from './SectionCollection.vue';
|
||||
import PageCollection from './PageCollection.vue';
|
||||
import uuid from 'uuid';
|
||||
|
||||
export default {
|
||||
@@ -139,8 +139,6 @@ export default {
|
||||
this.addSection();
|
||||
}
|
||||
},
|
||||
destroyed() {
|
||||
},
|
||||
methods: {
|
||||
addPage() {
|
||||
const pageTitle = this.pageTitle;
|
||||
@@ -13,7 +13,7 @@
|
||||
|
||||
<div class="l-browse-bar__end">
|
||||
<div class="l-browse-bar__snapshot-datetime">
|
||||
SNAPSHOT {{formatTime(embed.createdOn, 'YYYY-MM-DD HH:mm:ss')}}
|
||||
SNAPSHOT {{ createdOn }}
|
||||
</div>
|
||||
<span class="c-button-set c-button-set--strip-h">
|
||||
<button
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import Notebook from './components/notebook.vue';
|
||||
import NotebookSnapshotIndicator from './components/notebook-snapshot-indicator.vue';
|
||||
import Notebook from './components/Notebook.vue';
|
||||
import NotebookSnapshotIndicator from './components/NotebookSnapshotIndicator.vue';
|
||||
import SnapshotContainer from './snapshot-container';
|
||||
import Vue from 'vue';
|
||||
|
||||
@@ -95,7 +95,8 @@ export default function NotebookPlugin() {
|
||||
template: '<NotebookSnapshotIndicator></NotebookSnapshotIndicator>'
|
||||
});
|
||||
const indicator = {
|
||||
element: notebookSnapshotIndicator.$mount().$el
|
||||
element: notebookSnapshotIndicator.$mount().$el,
|
||||
key: 'notebook-snapshot-indicator'
|
||||
};
|
||||
|
||||
openmct.indicators.add(indicator);
|
||||
|
||||
222
src/plugins/notebook/pluginSpec.js
Normal file
222
src/plugins/notebook/pluginSpec.js
Normal file
@@ -0,0 +1,222 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2020, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
import { createOpenMct, createMouseEvent, resetApplicationState } from 'utils/testing';
|
||||
import NotebookPlugin from './plugin';
|
||||
import Vue from 'vue';
|
||||
|
||||
let openmct;
|
||||
let notebookDefinition;
|
||||
let notebookPlugin;
|
||||
let element;
|
||||
let child;
|
||||
let appHolder;
|
||||
|
||||
const notebookDomainObject = {
|
||||
identifier: {
|
||||
key: 'notebook',
|
||||
namespace: ''
|
||||
},
|
||||
type: 'notebook'
|
||||
};
|
||||
|
||||
describe("Notebook plugin:", () => {
|
||||
beforeAll(done => {
|
||||
appHolder = document.createElement('div');
|
||||
appHolder.style.width = '640px';
|
||||
appHolder.style.height = '480px';
|
||||
|
||||
openmct = createOpenMct();
|
||||
|
||||
element = document.createElement('div');
|
||||
child = document.createElement('div');
|
||||
element.appendChild(child);
|
||||
|
||||
notebookPlugin = new NotebookPlugin();
|
||||
openmct.install(notebookPlugin);
|
||||
|
||||
notebookDefinition = openmct.types.get('notebook').definition;
|
||||
notebookDefinition.initialize(notebookDomainObject);
|
||||
|
||||
openmct.on('start', done);
|
||||
openmct.start(appHolder);
|
||||
|
||||
document.body.append(appHolder);
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
appHolder.remove();
|
||||
resetApplicationState(openmct);
|
||||
});
|
||||
|
||||
it("has type as Notebook", () => {
|
||||
expect(notebookDefinition.name).toEqual('Notebook');
|
||||
});
|
||||
|
||||
it("is creatable", () => {
|
||||
expect(notebookDefinition.creatable).toEqual(true);
|
||||
});
|
||||
|
||||
describe("Notebook view:", () => {
|
||||
let notebookViewProvider;
|
||||
let notebookView;
|
||||
|
||||
beforeEach(() => {
|
||||
const notebookViewObject = {
|
||||
...notebookDomainObject,
|
||||
id: "test-object",
|
||||
name: 'Notebook',
|
||||
configuration: {
|
||||
defaultSort: 'oldest',
|
||||
entries: {},
|
||||
pageTitle: 'Page',
|
||||
sections: [],
|
||||
sectionTitle: 'Section',
|
||||
type: 'General'
|
||||
}
|
||||
};
|
||||
|
||||
const notebookObject = {
|
||||
name: 'Notebook View',
|
||||
key: 'notebook-vue',
|
||||
creatable: true
|
||||
};
|
||||
|
||||
const applicableViews = openmct.objectViews.get(notebookViewObject);
|
||||
notebookViewProvider = applicableViews.find(viewProvider => viewProvider.key === notebookObject.key);
|
||||
notebookView = notebookViewProvider.view(notebookViewObject);
|
||||
|
||||
notebookView.show(child);
|
||||
|
||||
return Vue.nextTick();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
notebookView.destroy();
|
||||
});
|
||||
|
||||
it("provides notebook view", () => {
|
||||
expect(notebookViewProvider).toBeDefined();
|
||||
});
|
||||
|
||||
it("renders notebook element", () => {
|
||||
const notebookElement = element.querySelectorAll('.c-notebook');
|
||||
expect(notebookElement.length).toBe(1);
|
||||
});
|
||||
|
||||
it("renders major elements", () => {
|
||||
const notebookElement = element.querySelector('.c-notebook');
|
||||
const searchElement = notebookElement.querySelector('.c-search');
|
||||
const sidebarElement = notebookElement.querySelector('.c-sidebar');
|
||||
const pageViewElement = notebookElement.querySelector('.c-notebook__page-view');
|
||||
const hasMajorElements = Boolean(searchElement && sidebarElement && pageViewElement);
|
||||
|
||||
expect(hasMajorElements).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Notebook Snapshots view:", () => {
|
||||
let snapshotIndicator;
|
||||
let drawerElement;
|
||||
|
||||
function clickSnapshotIndicator() {
|
||||
const indicator = element.querySelector('.icon-notebook');
|
||||
const button = indicator.querySelector('button');
|
||||
const clickEvent = createMouseEvent('click');
|
||||
|
||||
button.dispatchEvent(clickEvent);
|
||||
}
|
||||
|
||||
beforeAll(() => {
|
||||
snapshotIndicator = openmct.indicators.indicatorObjects
|
||||
.find(indicator => indicator.key === 'notebook-snapshot-indicator').element;
|
||||
|
||||
element.append(snapshotIndicator);
|
||||
|
||||
return Vue.nextTick();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
snapshotIndicator.remove();
|
||||
if (drawerElement) {
|
||||
drawerElement.remove();
|
||||
}
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
drawerElement = document.querySelector('.l-shell__drawer');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (drawerElement) {
|
||||
drawerElement.classList.remove('is-expanded');
|
||||
}
|
||||
});
|
||||
|
||||
it("has Snapshots indicator", () => {
|
||||
const hasSnapshotIndicator = snapshotIndicator !== null && snapshotIndicator !== undefined;
|
||||
expect(hasSnapshotIndicator).toBe(true);
|
||||
});
|
||||
|
||||
it("snapshots container has class isExpanded", () => {
|
||||
let classes = drawerElement.classList;
|
||||
const isExpandedBefore = classes.contains('is-expanded');
|
||||
|
||||
clickSnapshotIndicator();
|
||||
classes = drawerElement.classList;
|
||||
const isExpandedAfterFirstClick = classes.contains('is-expanded');
|
||||
|
||||
const success = isExpandedBefore === false
|
||||
&& isExpandedAfterFirstClick === true;
|
||||
|
||||
expect(success).toBe(true);
|
||||
});
|
||||
|
||||
it("snapshots container does not have class isExpanded", () => {
|
||||
let classes = drawerElement.classList;
|
||||
const isExpandedBefore = classes.contains('is-expanded');
|
||||
|
||||
clickSnapshotIndicator();
|
||||
classes = drawerElement.classList;
|
||||
const isExpandedAfterFirstClick = classes.contains('is-expanded');
|
||||
|
||||
clickSnapshotIndicator();
|
||||
classes = drawerElement.classList;
|
||||
const isExpandedAfterSecondClick = classes.contains('is-expanded');
|
||||
|
||||
const success = isExpandedBefore === false
|
||||
&& isExpandedAfterFirstClick === true
|
||||
&& isExpandedAfterSecondClick === false;
|
||||
|
||||
expect(success).toBe(true);
|
||||
});
|
||||
|
||||
it("show notebook snapshots container text", () => {
|
||||
clickSnapshotIndicator();
|
||||
|
||||
const notebookSnapshots = drawerElement.querySelector('.l-browse-bar__object-name');
|
||||
const snapshotsText = notebookSnapshots.textContent.trim();
|
||||
|
||||
expect(snapshotsText).toBe('Notebook Snapshots');
|
||||
});
|
||||
});
|
||||
});
|
||||
195
src/plugins/notebook/utils/notebook-entriesSpec.js
Normal file
195
src/plugins/notebook/utils/notebook-entriesSpec.js
Normal file
@@ -0,0 +1,195 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2020, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* 'License'); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an 'AS IS' BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
import * as NotebookEntries from './notebook-entries';
|
||||
import { createOpenMct, spyOnBuiltins, resetApplicationState } from 'utils/testing';
|
||||
|
||||
const notebookStorage = {
|
||||
domainObject: {
|
||||
name: 'notebook',
|
||||
identifier: {
|
||||
namespace: '',
|
||||
key: 'test-notebook'
|
||||
}
|
||||
},
|
||||
notebookMeta: {
|
||||
name: 'notebook',
|
||||
identifier: {
|
||||
namespace: '',
|
||||
key: 'test-notebook'
|
||||
}
|
||||
},
|
||||
section: {
|
||||
id: '03a79b6a-971c-4e56-9892-ec536332c3f0',
|
||||
isDefault: true,
|
||||
isSelected: true,
|
||||
name: 'section',
|
||||
pages: [],
|
||||
sectionTitle: 'Section'
|
||||
},
|
||||
page: {
|
||||
id: '8b548fd9-2b8a-4b02-93a9-4138e22eba00',
|
||||
isDefault: true,
|
||||
isSelected: true,
|
||||
name: 'page',
|
||||
pageTitle: 'Page'
|
||||
}
|
||||
};
|
||||
|
||||
const notebookEntries = {
|
||||
'03a79b6a-971c-4e56-9892-ec536332c3f0': {
|
||||
'8b548fd9-2b8a-4b02-93a9-4138e22eba00': []
|
||||
}
|
||||
};
|
||||
|
||||
const notebookDomainObject = {
|
||||
identifier: {
|
||||
key: 'notebook',
|
||||
namespace: ''
|
||||
},
|
||||
type: 'notebook',
|
||||
configuration: {
|
||||
defaultSort: 'oldest',
|
||||
entries: notebookEntries,
|
||||
pageTitle: 'Page',
|
||||
sections: [],
|
||||
sectionTitle: 'Section',
|
||||
type: 'General'
|
||||
}
|
||||
};
|
||||
|
||||
const selectedSection = {
|
||||
id: '03a79b6a-971c-4e56-9892-ec536332c3f0',
|
||||
isDefault: false,
|
||||
isSelected: true,
|
||||
name: 'Day 1',
|
||||
pages: [
|
||||
{
|
||||
id: '54deb3d5-8267-4be4-95e9-3579ed8c082d',
|
||||
isDefault: false,
|
||||
isSelected: false,
|
||||
name: 'Shift 1',
|
||||
pageTitle: 'Page'
|
||||
},
|
||||
{
|
||||
id: '2ea41c78-8e60-4657-a350-53f1a1fa3021',
|
||||
isDefault: false,
|
||||
isSelected: false,
|
||||
name: 'Shift 2',
|
||||
pageTitle: 'Page'
|
||||
},
|
||||
{
|
||||
id: '8b548fd9-2b8a-4b02-93a9-4138e22eba00',
|
||||
isDefault: false,
|
||||
isSelected: true,
|
||||
name: 'Unnamed Page',
|
||||
pageTitle: 'Page'
|
||||
}
|
||||
],
|
||||
sectionTitle: 'Section'
|
||||
};
|
||||
|
||||
const selectedPage = {
|
||||
id: '8b548fd9-2b8a-4b02-93a9-4138e22eba00',
|
||||
isDefault: false,
|
||||
isSelected: true,
|
||||
name: 'Unnamed Page',
|
||||
pageTitle: 'Page'
|
||||
};
|
||||
|
||||
let openmct;
|
||||
|
||||
describe('Notebook Entries:', () => {
|
||||
beforeEach(done => {
|
||||
openmct = createOpenMct();
|
||||
window.localStorage.setItem('notebook-storage', null);
|
||||
spyOnBuiltins(['mutate'], openmct.objects);
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
notebookDomainObject.configuration.entries[selectedSection.id][selectedPage.id] = [];
|
||||
resetApplicationState(openmct);
|
||||
});
|
||||
|
||||
it('getNotebookEntries has no entries', () => {
|
||||
const entries = NotebookEntries.getNotebookEntries(notebookDomainObject, selectedSection, selectedPage);
|
||||
|
||||
expect(entries.length).toEqual(0);
|
||||
});
|
||||
|
||||
it('addNotebookEntry mutates object', () => {
|
||||
NotebookEntries.addNotebookEntry(openmct, notebookDomainObject, notebookStorage);
|
||||
|
||||
expect(openmct.objects.mutate).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('addNotebookEntry adds entry', () => {
|
||||
NotebookEntries.addNotebookEntry(openmct, notebookDomainObject, notebookStorage);
|
||||
const entries = NotebookEntries.getNotebookEntries(notebookDomainObject, selectedSection, selectedPage);
|
||||
|
||||
expect(entries.length).toEqual(1);
|
||||
});
|
||||
|
||||
it('getEntryPosById returns valid position', () => {
|
||||
const entryId = NotebookEntries.addNotebookEntry(openmct, notebookDomainObject, notebookStorage);
|
||||
const position = NotebookEntries.getEntryPosById(entryId, notebookDomainObject, selectedSection, selectedPage);
|
||||
|
||||
expect(position).toEqual(0);
|
||||
});
|
||||
|
||||
it('getEntryPosById returns valid position', () => {
|
||||
const entryId1 = NotebookEntries.addNotebookEntry(openmct, notebookDomainObject, notebookStorage);
|
||||
const position1 = NotebookEntries.getEntryPosById(entryId1, notebookDomainObject, selectedSection, selectedPage);
|
||||
|
||||
const entryId2 = NotebookEntries.addNotebookEntry(openmct, notebookDomainObject, notebookStorage);
|
||||
const position2 = NotebookEntries.getEntryPosById(entryId2, notebookDomainObject, selectedSection, selectedPage);
|
||||
|
||||
const entryId3 = NotebookEntries.addNotebookEntry(openmct, notebookDomainObject, notebookStorage);
|
||||
const position3 = NotebookEntries.getEntryPosById(entryId3, notebookDomainObject, selectedSection, selectedPage);
|
||||
|
||||
const success = position1 === 0
|
||||
&& position2 === 1
|
||||
&& position3 === 2;
|
||||
|
||||
expect(success).toBe(true);
|
||||
});
|
||||
|
||||
it('deleteNotebookEntries mutates object', () => {
|
||||
openmct.objects.mutate.calls.reset();
|
||||
|
||||
NotebookEntries.addNotebookEntry(openmct, notebookDomainObject, notebookStorage);
|
||||
NotebookEntries.deleteNotebookEntries(openmct, notebookDomainObject, selectedSection, selectedPage);
|
||||
|
||||
expect(openmct.objects.mutate).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('deleteNotebookEntries deletes correct entry', () => {
|
||||
NotebookEntries.addNotebookEntry(openmct, notebookDomainObject, notebookStorage);
|
||||
NotebookEntries.addNotebookEntry(openmct, notebookDomainObject, notebookStorage);
|
||||
|
||||
NotebookEntries.deleteNotebookEntries(openmct, notebookDomainObject, selectedSection, selectedPage);
|
||||
const afterEntries = NotebookEntries.getNotebookEntries(notebookDomainObject, selectedSection, selectedPage);
|
||||
|
||||
expect(afterEntries).toEqual(null);
|
||||
});
|
||||
});
|
||||
125
src/plugins/notebook/utils/notebook-storageSpec.js
Normal file
125
src/plugins/notebook/utils/notebook-storageSpec.js
Normal file
@@ -0,0 +1,125 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2020, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
import * as NotebookStorage from './notebook-storage';
|
||||
import { createOpenMct, resetApplicationState } from 'utils/testing';
|
||||
|
||||
const notebookStorage = {
|
||||
domainObject: {
|
||||
name: 'notebook',
|
||||
identifier: {
|
||||
namespace: '',
|
||||
key: 'test-notebook'
|
||||
}
|
||||
},
|
||||
notebookMeta: {
|
||||
name: 'notebook',
|
||||
identifier: {
|
||||
namespace: '',
|
||||
key: 'test-notebook'
|
||||
}
|
||||
},
|
||||
section: {
|
||||
id: 'temp-section',
|
||||
isDefault: false,
|
||||
isSelected: true,
|
||||
name: 'section',
|
||||
pages: [],
|
||||
sectionTitle: 'Section'
|
||||
},
|
||||
page: {
|
||||
id: 'temp-page',
|
||||
isDefault: false,
|
||||
isSelected: true,
|
||||
name: 'page',
|
||||
pageTitle: 'Page'
|
||||
}
|
||||
};
|
||||
|
||||
let openmct = createOpenMct();
|
||||
|
||||
describe('Notebook Storage:', () => {
|
||||
beforeEach((done) => {
|
||||
openmct = createOpenMct();
|
||||
window.localStorage.setItem('notebook-storage', null);
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
resetApplicationState(openmct);
|
||||
});
|
||||
|
||||
it('has empty local Storage', () => {
|
||||
expect(window.localStorage).not.toBeNull();
|
||||
});
|
||||
|
||||
it('has null notebookstorage on clearDefaultNotebook', () => {
|
||||
window.localStorage.setItem('notebook-storage', notebookStorage);
|
||||
NotebookStorage.clearDefaultNotebook();
|
||||
const defaultNotebook = NotebookStorage.getDefaultNotebook();
|
||||
|
||||
expect(defaultNotebook).toBeNull();
|
||||
});
|
||||
|
||||
it('has correct notebookstorage on setDefaultNotebook', () => {
|
||||
NotebookStorage.setDefaultNotebook(openmct, notebookStorage);
|
||||
const defaultNotebook = NotebookStorage.getDefaultNotebook();
|
||||
|
||||
expect(JSON.stringify(defaultNotebook)).toBe(JSON.stringify(notebookStorage));
|
||||
});
|
||||
|
||||
it('has correct section on setDefaultNotebookSection', () => {
|
||||
const section = {
|
||||
id: 'new-temp-section',
|
||||
isDefault: true,
|
||||
isSelected: true,
|
||||
name: 'new section',
|
||||
pages: [],
|
||||
sectionTitle: 'Section'
|
||||
};
|
||||
|
||||
NotebookStorage.setDefaultNotebook(openmct, notebookStorage);
|
||||
NotebookStorage.setDefaultNotebookSection(section);
|
||||
|
||||
const defaultNotebook = NotebookStorage.getDefaultNotebook();
|
||||
const newSection = defaultNotebook.section;
|
||||
expect(JSON.stringify(section)).toBe(JSON.stringify(newSection));
|
||||
});
|
||||
|
||||
it('has correct page on setDefaultNotebookPage', () => {
|
||||
const page = {
|
||||
id: 'new-temp-page',
|
||||
isDefault: true,
|
||||
isSelected: true,
|
||||
name: 'new page',
|
||||
pageTitle: 'Page'
|
||||
};
|
||||
|
||||
NotebookStorage.setDefaultNotebook(openmct, notebookStorage);
|
||||
NotebookStorage.setDefaultNotebookPage(page);
|
||||
|
||||
const defaultNotebook = NotebookStorage.getDefaultNotebook();
|
||||
const newPage = defaultNotebook.page;
|
||||
expect(JSON.stringify(page)).toBe(JSON.stringify(newPage));
|
||||
});
|
||||
});
|
||||
79
src/plugins/notebook/utils/painterroInstance.js
Normal file
79
src/plugins/notebook/utils/painterroInstance.js
Normal file
@@ -0,0 +1,79 @@
|
||||
import Painterro from 'painterro';
|
||||
|
||||
const DEFAULT_CONFIG = {
|
||||
activeColor: '#ff0000',
|
||||
activeColorAlpha: 1.0,
|
||||
activeFillColor: '#fff',
|
||||
activeFillColorAlpha: 0.0,
|
||||
backgroundFillColor: '#000',
|
||||
backgroundFillColorAlpha: 0.0,
|
||||
defaultFontSize: 16,
|
||||
defaultLineWidth: 2,
|
||||
defaultTool: 'ellipse',
|
||||
hiddenTools: ['save', 'open', 'close', 'eraser', 'pixelize', 'rotate', 'settings', 'resize'],
|
||||
translation: {
|
||||
name: 'en',
|
||||
strings: {
|
||||
lineColor: 'Line',
|
||||
fillColor: 'Fill',
|
||||
lineWidth: 'Size',
|
||||
textColor: 'Color',
|
||||
fontSize: 'Size',
|
||||
fontStyle: 'Style'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default class PainterroInstance {
|
||||
constructor(element, saveCallback) {
|
||||
this.elementId = element.id;
|
||||
this.isSave = false;
|
||||
this.painterroInstance = null;
|
||||
this.saveCallback = saveCallback;
|
||||
}
|
||||
|
||||
dismiss() {
|
||||
this.isSave = false;
|
||||
this.painterroInstance.save();
|
||||
}
|
||||
|
||||
intialize() {
|
||||
this.config = Object.assign({}, DEFAULT_CONFIG);
|
||||
|
||||
this.config.id = this.elementId;
|
||||
this.config.saveHandler = this.saveHandler.bind(this);
|
||||
|
||||
this.painterro = Painterro(this.config);
|
||||
}
|
||||
|
||||
save() {
|
||||
this.isSave = true;
|
||||
this.painterroInstance.save();
|
||||
}
|
||||
|
||||
saveHandler(image, done) {
|
||||
if (this.isSave) {
|
||||
const self = this;
|
||||
const url = image.asBlob();
|
||||
const reader = new window.FileReader();
|
||||
reader.readAsDataURL(url);
|
||||
reader.onloadend = () => {
|
||||
const snapshot = reader.result;
|
||||
const snapshotObject = {
|
||||
src: snapshot,
|
||||
type: url.type,
|
||||
size: url.size,
|
||||
modified: Date.now()
|
||||
};
|
||||
|
||||
self.saveCallback(snapshotObject);
|
||||
};
|
||||
}
|
||||
|
||||
done(true);
|
||||
}
|
||||
|
||||
show(src) {
|
||||
this.painterroInstance = this.painterro.show(src);
|
||||
}
|
||||
}
|
||||
@@ -57,7 +57,8 @@ define([
|
||||
'./notificationIndicator/plugin',
|
||||
'./newFolderAction/plugin',
|
||||
'./persistence/couch/plugin',
|
||||
'./defaultRootName/plugin'
|
||||
'./defaultRootName/plugin',
|
||||
'./timeline/plugin'
|
||||
], function (
|
||||
_,
|
||||
UTCTimeSystem,
|
||||
@@ -95,7 +96,8 @@ define([
|
||||
NotificationIndicator,
|
||||
NewFolderAction,
|
||||
CouchDBPlugin,
|
||||
DefaultRootName
|
||||
DefaultRootName,
|
||||
Timeline
|
||||
) {
|
||||
const bundleMap = {
|
||||
LocalStorage: 'platform/persistence/local',
|
||||
@@ -188,6 +190,7 @@ define([
|
||||
plugins.NewFolderAction = NewFolderAction.default;
|
||||
plugins.ISOTimeFormat = ISOTimeFormat.default;
|
||||
plugins.DefaultRootName = DefaultRootName.default;
|
||||
plugins.Timeline = Timeline.default;
|
||||
|
||||
return plugins;
|
||||
});
|
||||
|
||||
437
src/plugins/timeline/Plan.vue
Normal file
437
src/plugins/timeline/Plan.vue
Normal file
@@ -0,0 +1,437 @@
|
||||
<template>
|
||||
<div ref="axisHolder"
|
||||
class="c-timeline-plan"
|
||||
>
|
||||
<div class="nowMarker"><span class="icon-arrow-down"></span></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import * as d3Selection from 'd3-selection';
|
||||
import * as d3Axis from 'd3-axis';
|
||||
import * as d3Scale from 'd3-scale';
|
||||
import utcMultiTimeFormat from "@/plugins/timeConductor/utcMultiTimeFormat";
|
||||
|
||||
//TODO: UI direction needed for the following property values
|
||||
const PADDING = 1;
|
||||
const OUTER_TEXT_PADDING = 12;
|
||||
const INNER_TEXT_PADDING = 17;
|
||||
const TEXT_LEFT_PADDING = 5;
|
||||
const ROW_PADDING = 12;
|
||||
// const DEFAULT_DURATION_FORMATTER = 'duration';
|
||||
const RESIZE_POLL_INTERVAL = 200;
|
||||
const PIXELS_PER_TICK = 100;
|
||||
const PIXELS_PER_TICK_WIDE = 200;
|
||||
const ROW_HEIGHT = 30;
|
||||
const LINE_HEIGHT = 12;
|
||||
const MAX_TEXT_WIDTH = 300;
|
||||
const TIMELINE_HEIGHT = 30;
|
||||
//This offset needs to be re-considered
|
||||
const TIMELINE_OFFSET_HEIGHT = 70;
|
||||
const GROUP_OFFSET = 100;
|
||||
|
||||
export default {
|
||||
inject: ['openmct', 'domainObject'],
|
||||
props: {
|
||||
"renderingEngine": {
|
||||
type: String,
|
||||
default() {
|
||||
return 'canvas';
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.validateJSON(this.domainObject.selectFile.body);
|
||||
if (this.renderingEngine === 'svg') {
|
||||
this.useSVG = true;
|
||||
}
|
||||
|
||||
this.container = d3Selection.select(this.$refs.axisHolder);
|
||||
this.svgElement = this.container.append("svg:svg");
|
||||
// draw x axis with labels. CSS is used to position them.
|
||||
this.axisElement = this.svgElement.append("g")
|
||||
.attr("class", "axis");
|
||||
this.xAxis = d3Axis.axisTop();
|
||||
|
||||
this.canvas = this.container.append('canvas').node();
|
||||
this.canvasContext = this.canvas.getContext('2d');
|
||||
|
||||
this.setDimensions();
|
||||
this.updateViewBounds();
|
||||
this.openmct.time.on("timeSystem", this.setScaleAndPlotActivities);
|
||||
this.openmct.time.on("bounds", this.updateViewBounds);
|
||||
this.resizeTimer = setInterval(this.resize, RESIZE_POLL_INTERVAL);
|
||||
},
|
||||
destroyed() {
|
||||
clearInterval(this.resizeTimer);
|
||||
this.openmct.time.off("timeSystem", this.setScaleAndPlotActivities);
|
||||
this.openmct.time.off("bounds", this.updateViewBounds);
|
||||
},
|
||||
methods: {
|
||||
resize() {
|
||||
if (this.$refs.axisHolder.clientWidth !== this.width) {
|
||||
this.setDimensions();
|
||||
this.updateViewBounds();
|
||||
}
|
||||
},
|
||||
validateJSON(jsonString) {
|
||||
try {
|
||||
this.json = JSON.parse(jsonString);
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
updateViewBounds() {
|
||||
this.viewBounds = this.openmct.time.bounds();
|
||||
// this.viewBounds.end = this.viewBounds.end + (30 * 60 * 1000);
|
||||
this.setScaleAndPlotActivities();
|
||||
},
|
||||
updateNowMarker() {
|
||||
if (this.openmct.time.clock() === undefined) {
|
||||
let nowMarker = document.querySelector('.nowMarker');
|
||||
if (nowMarker) {
|
||||
nowMarker.parentNode.removeChild(nowMarker);
|
||||
}
|
||||
} else {
|
||||
let nowMarker = document.querySelector('.nowMarker');
|
||||
if (nowMarker) {
|
||||
const svgEl = d3Selection.select(this.svgElement).node();
|
||||
const height = this.useSVG ? svgEl.style('height') : this.canvas.height + 'px';
|
||||
nowMarker.style.height = height;
|
||||
const now = this.xScale(Date.now());
|
||||
nowMarker.style.left = now + GROUP_OFFSET + 'px';
|
||||
}
|
||||
}
|
||||
},
|
||||
setScaleAndPlotActivities() {
|
||||
this.setScale();
|
||||
this.clearPreviousActivities();
|
||||
if (this.xScale) {
|
||||
this.calculatePlanLayout();
|
||||
this.drawPlan();
|
||||
this.updateNowMarker();
|
||||
}
|
||||
},
|
||||
clearPreviousActivities() {
|
||||
if (this.useSVG) {
|
||||
d3Selection.selectAll("svg > :not(g)").remove();
|
||||
} else {
|
||||
this.canvasContext.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
||||
}
|
||||
},
|
||||
setDimensions() {
|
||||
const axisHolder = this.$refs.axisHolder;
|
||||
const rect = axisHolder.getBoundingClientRect();
|
||||
this.left = Math.round(rect.left);
|
||||
this.top = Math.round(rect.top);
|
||||
this.width = axisHolder.clientWidth;
|
||||
this.offsetWidth = this.width - GROUP_OFFSET;
|
||||
|
||||
const axisHolderParent = this.$parent.$refs.planHolder;
|
||||
this.height = Math.round(axisHolderParent.getBoundingClientRect().height);
|
||||
|
||||
if (this.useSVG) {
|
||||
this.svgElement.attr("width", this.width);
|
||||
this.svgElement.attr("height", this.height);
|
||||
} else {
|
||||
this.svgElement.attr("height", 50);
|
||||
this.canvas.width = this.width;
|
||||
this.canvas.height = this.height;
|
||||
}
|
||||
|
||||
this.canvasContext.font = "normal normal 12px sans-serif";
|
||||
},
|
||||
setScale(timeSystem) {
|
||||
if (!this.width) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (timeSystem === undefined) {
|
||||
timeSystem = this.openmct.time.timeSystem();
|
||||
}
|
||||
|
||||
if (timeSystem.isUTCBased) {
|
||||
this.xScale = d3Scale.scaleUtc();
|
||||
this.xScale.domain(
|
||||
[new Date(this.viewBounds.start), new Date(this.viewBounds.end)]
|
||||
);
|
||||
} else {
|
||||
this.xScale = d3Scale.scaleLinear();
|
||||
this.xScale.domain(
|
||||
[this.viewBounds.start, this.viewBounds.end]
|
||||
);
|
||||
}
|
||||
|
||||
this.xScale.range([PADDING, this.offsetWidth - PADDING * 2]);
|
||||
|
||||
this.xAxis.scale(this.xScale);
|
||||
this.xAxis.tickFormat(utcMultiTimeFormat);
|
||||
|
||||
this.axisElement.call(this.xAxis);
|
||||
|
||||
if (this.width > 1800) {
|
||||
this.xAxis.ticks(this.offsetWidth / PIXELS_PER_TICK_WIDE);
|
||||
} else {
|
||||
this.xAxis.ticks(this.offsetWidth / PIXELS_PER_TICK);
|
||||
}
|
||||
},
|
||||
isActivityInBounds(activity) {
|
||||
return (activity.start < this.viewBounds.end) && (activity.end > this.viewBounds.start);
|
||||
},
|
||||
getTextWidth(name) {
|
||||
// canvasContext.font = font;
|
||||
let metrics = this.canvasContext.measureText(name);
|
||||
|
||||
return parseInt(metrics.width, 10);
|
||||
},
|
||||
sortFn(a, b) {
|
||||
const numA = parseInt(a, 10);
|
||||
const numB = parseInt(b, 10);
|
||||
if (numA > numB) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (numA < numB) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
},
|
||||
// Get the row where the next activity will land.
|
||||
getRowForActivity(rectX, width, defaultActivityRow = 0) {
|
||||
let currentRow;
|
||||
let sortedActivityRows = Object.keys(this.activitiesByRow).sort(this.sortFn);
|
||||
|
||||
function getOverlap(rects) {
|
||||
return rects.every(rect => {
|
||||
const { start, end } = rect;
|
||||
const calculatedEnd = rectX + width;
|
||||
const hasOverlap = (rectX >= start && rectX <= end) || (calculatedEnd >= start && calculatedEnd <= end) || (rectX <= start && calculatedEnd >= end);
|
||||
|
||||
return !hasOverlap;
|
||||
});
|
||||
}
|
||||
|
||||
for (let i = 0; i < sortedActivityRows.length; i++) {
|
||||
let row = sortedActivityRows[i];
|
||||
if (getOverlap(this.activitiesByRow[row])) {
|
||||
currentRow = row;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (currentRow === undefined && sortedActivityRows.length) {
|
||||
currentRow = parseInt(sortedActivityRows[sortedActivityRows.length - 1], 10) + ROW_HEIGHT + ROW_PADDING;
|
||||
}
|
||||
|
||||
return (currentRow || defaultActivityRow);
|
||||
},
|
||||
calculatePlanLayout() {
|
||||
this.activitiesByRow = {};
|
||||
|
||||
let currentRow = 0;
|
||||
|
||||
let groups = Object.keys(this.json);
|
||||
groups.forEach((key, index) => {
|
||||
let activities = this.json[key];
|
||||
//set the currentRow to the beginning of the next logical row
|
||||
currentRow = currentRow + ROW_HEIGHT * index;
|
||||
let newGroup = true;
|
||||
activities.forEach((activity) => {
|
||||
if (this.isActivityInBounds(activity)) {
|
||||
const currentStart = Math.max(this.viewBounds.start, activity.start);
|
||||
const currentEnd = Math.min(this.viewBounds.end, activity.end);
|
||||
const rectX = this.xScale(currentStart);
|
||||
const rectY = this.xScale(currentEnd);
|
||||
const rectWidth = rectY - rectX;
|
||||
|
||||
const activityNameWidth = this.getTextWidth(activity.name) + TEXT_LEFT_PADDING;
|
||||
//TODO: Fix bug for SVG where the rectWidth is not proportional to the canvas measuredWidth of the text
|
||||
const activityNameFitsRect = (rectWidth >= activityNameWidth);
|
||||
const textStart = (activityNameFitsRect ? rectX : (rectX + rectWidth)) + TEXT_LEFT_PADDING;
|
||||
|
||||
let textLines = this.getActivityDisplayText(this.canvasContext, activity.name, activityNameFitsRect);
|
||||
const textWidth = textStart + this.getTextWidth(textLines[0]) + TEXT_LEFT_PADDING;
|
||||
|
||||
if (activityNameFitsRect) {
|
||||
currentRow = this.getRowForActivity(rectX, rectWidth);
|
||||
} else {
|
||||
currentRow = this.getRowForActivity(rectX, textWidth);
|
||||
}
|
||||
|
||||
let textY = parseInt(currentRow, 10) + (activityNameFitsRect ? INNER_TEXT_PADDING : OUTER_TEXT_PADDING);
|
||||
|
||||
if (!this.activitiesByRow[currentRow]) {
|
||||
this.activitiesByRow[currentRow] = [];
|
||||
}
|
||||
|
||||
this.activitiesByRow[currentRow].push({
|
||||
heading: newGroup ? key : '',
|
||||
activity: {
|
||||
color: activity.color,
|
||||
textColor: activity.textColor
|
||||
},
|
||||
textLines: textLines,
|
||||
textStart: textStart,
|
||||
textY: textY,
|
||||
start: rectX,
|
||||
end: activityNameFitsRect ? rectX + rectWidth : textStart + textWidth,
|
||||
rectWidth: rectWidth
|
||||
});
|
||||
newGroup = false;
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
getActivityDisplayText(context, text, activityNameFitsRect) {
|
||||
//TODO: If the activity start is less than viewBounds.start then the text should be cropped on the left/should be off-screen)
|
||||
let words = text.split(' ');
|
||||
let line = '';
|
||||
let activityText = [];
|
||||
let rows = 1;
|
||||
|
||||
for (let n = 0; (n < words.length) && (rows <= 2); n++) {
|
||||
let testLine = line + words[n] + ' ';
|
||||
let metrics = context.measureText(testLine);
|
||||
let testWidth = metrics.width;
|
||||
if (!activityNameFitsRect && (testWidth > MAX_TEXT_WIDTH && n > 0)) {
|
||||
activityText.push(line);
|
||||
line = words[n] + ' ';
|
||||
testLine = line + words[n] + ' ';
|
||||
rows = rows + 1;
|
||||
}
|
||||
|
||||
line = testLine;
|
||||
}
|
||||
|
||||
return activityText.length ? activityText : [line];
|
||||
},
|
||||
getGroupHeading(row) {
|
||||
let groupHeadingRow;
|
||||
let groupHeadingBorder;
|
||||
|
||||
if (row) {
|
||||
groupHeadingBorder = row + ROW_PADDING + OUTER_TEXT_PADDING;
|
||||
groupHeadingRow = groupHeadingBorder + OUTER_TEXT_PADDING;
|
||||
} else {
|
||||
groupHeadingRow = TIMELINE_HEIGHT + OUTER_TEXT_PADDING;
|
||||
}
|
||||
|
||||
return {
|
||||
groupHeadingRow,
|
||||
groupHeadingBorder
|
||||
};
|
||||
},
|
||||
getPlanHeight(activityRows) {
|
||||
return parseInt(activityRows[activityRows.length - 1], 10) + TIMELINE_OFFSET_HEIGHT;
|
||||
},
|
||||
drawPlan() {
|
||||
const activityRows = Object.keys(this.activitiesByRow);
|
||||
if (activityRows.length) {
|
||||
|
||||
let planHeight = this.getPlanHeight(activityRows);
|
||||
planHeight = Math.max(this.height, planHeight);
|
||||
if (this.useSVG) {
|
||||
this.svgElement.attr("height", planHeight);
|
||||
} else {
|
||||
// This needs to happen before we draw on the canvas or the canvas will get wiped out when height is set
|
||||
this.canvas.height = planHeight;
|
||||
}
|
||||
|
||||
activityRows.forEach((key) => {
|
||||
const items = this.activitiesByRow[key];
|
||||
const row = parseInt(key, 10);
|
||||
items.forEach((item) => {
|
||||
|
||||
//TODO: Don't draw the left-border of the rectangle if the activity started before viewBounds.start
|
||||
if (this.useSVG) {
|
||||
this.plotSVG(item, row);
|
||||
} else {
|
||||
this.plotCanvas(item, row);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
},
|
||||
plotSVG(item, row) {
|
||||
const headingText = item.heading;
|
||||
const { groupHeadingRow, groupHeadingBorder } = this.getGroupHeading(row);
|
||||
|
||||
if (headingText) {
|
||||
if (groupHeadingBorder) {
|
||||
this.svgElement.append("line")
|
||||
.attr("class", "activity")
|
||||
.attr("x1", 0)
|
||||
.attr("y1", groupHeadingBorder)
|
||||
.attr("x2", this.width)
|
||||
.attr("y2", groupHeadingBorder)
|
||||
.attr('stroke', "white");
|
||||
}
|
||||
|
||||
this.svgElement.append("text").text(headingText)
|
||||
.attr("class", "activity")
|
||||
.attr("x", 0)
|
||||
.attr("y", groupHeadingRow)
|
||||
.attr('fill', "white");
|
||||
}
|
||||
|
||||
const activity = item.activity;
|
||||
const rectY = row + TIMELINE_HEIGHT;
|
||||
this.svgElement.append("rect")
|
||||
.attr("class", "activity")
|
||||
.attr("x", item.start + GROUP_OFFSET)
|
||||
.attr("y", rectY + TIMELINE_HEIGHT)
|
||||
.attr("width", item.rectWidth)
|
||||
.attr("height", ROW_HEIGHT)
|
||||
.attr('fill', activity.color)
|
||||
.attr('stroke', "lightgray");
|
||||
|
||||
item.textLines.forEach((line, index) => {
|
||||
this.svgElement.append("text").text(line)
|
||||
.attr("class", "activity")
|
||||
.attr("x", item.textStart + GROUP_OFFSET)
|
||||
.attr("y", item.textY + TIMELINE_HEIGHT + (index * LINE_HEIGHT))
|
||||
.attr('fill', activity.textColor);
|
||||
});
|
||||
//TODO: Ending border
|
||||
},
|
||||
plotCanvas(item, row) {
|
||||
const headingText = item.heading;
|
||||
const { groupHeadingRow, groupHeadingBorder } = this.getGroupHeading(row);
|
||||
|
||||
if (headingText) {
|
||||
if (groupHeadingBorder) {
|
||||
this.canvasContext.strokeStyle = "white";
|
||||
this.canvasContext.beginPath();
|
||||
this.canvasContext.moveTo(0, groupHeadingBorder);
|
||||
this.canvasContext.lineTo(this.width, groupHeadingBorder);
|
||||
this.canvasContext.stroke();
|
||||
}
|
||||
|
||||
this.canvasContext.fillStyle = "white";
|
||||
this.canvasContext.fillText(headingText, 0, groupHeadingRow);
|
||||
}
|
||||
|
||||
const activity = item.activity;
|
||||
const rectX = item.start;
|
||||
const rectY = row + TIMELINE_HEIGHT;
|
||||
const rectWidth = item.rectWidth;
|
||||
this.canvasContext.fillStyle = activity.color;
|
||||
this.canvasContext.strokeStyle = "lightgray";
|
||||
this.canvasContext.fillRect(rectX + GROUP_OFFSET, rectY, rectWidth, ROW_HEIGHT);
|
||||
this.canvasContext.strokeRect(rectX + GROUP_OFFSET, rectY, rectWidth, ROW_HEIGHT);
|
||||
|
||||
this.canvasContext.fillStyle = activity.textColor;
|
||||
|
||||
item.textLines.forEach((line, index) => {
|
||||
this.canvasContext.fillText(line, item.textStart + GROUP_OFFSET, item.textY + TIMELINE_HEIGHT + (index * LINE_HEIGHT));
|
||||
});
|
||||
//TODO: Ending border
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
45
src/plugins/timeline/TimelineViewLayout.vue
Normal file
45
src/plugins/timeline/TimelineViewLayout.vue
Normal file
@@ -0,0 +1,45 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2020, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
<template>
|
||||
<div ref="planHolder"
|
||||
class="c-timeline"
|
||||
>
|
||||
<plan :rendering-engine="'canvas'" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Plan from './Plan.vue';
|
||||
|
||||
export default {
|
||||
inject: ['openmct', 'domainObject'],
|
||||
components: {
|
||||
Plan
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
plans: []
|
||||
};
|
||||
}
|
||||
};
|
||||
</script>
|
||||
64
src/plugins/timeline/TimelineViewProvider.js
Normal file
64
src/plugins/timeline/TimelineViewProvider.js
Normal file
@@ -0,0 +1,64 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2020, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
import TimelineViewLayout from './TimelineViewLayout.vue';
|
||||
import Vue from 'vue';
|
||||
|
||||
export default function TimelineViewProvider(openmct) {
|
||||
|
||||
return {
|
||||
key: 'timeline.view',
|
||||
name: 'Timeline',
|
||||
cssClass: 'icon-clock',
|
||||
canView(domainObject) {
|
||||
return domainObject.type === 'plan';
|
||||
},
|
||||
|
||||
canEdit(domainObject) {
|
||||
return domainObject.type === 'plan';
|
||||
},
|
||||
|
||||
view: function (domainObject) {
|
||||
let component;
|
||||
|
||||
return {
|
||||
show: function (element) {
|
||||
component = new Vue({
|
||||
el: element,
|
||||
components: {
|
||||
TimelineViewLayout
|
||||
},
|
||||
provide: {
|
||||
openmct,
|
||||
domainObject
|
||||
},
|
||||
template: '<timeline-view-layout></timeline-view-layout>'
|
||||
});
|
||||
},
|
||||
destroy: function () {
|
||||
component.$destroy();
|
||||
component = undefined;
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
38
src/plugins/timeline/activities.json
Normal file
38
src/plugins/timeline/activities.json
Normal file
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"ROVER": [
|
||||
{
|
||||
"name": "Activity 1",
|
||||
"start": 1597170002854,
|
||||
"end": 1597171032854,
|
||||
"type": "ROVER",
|
||||
"color": "fuchsia",
|
||||
"textColor": "black"
|
||||
},
|
||||
{
|
||||
"name": "Activity 2",
|
||||
"start": 1597171132854,
|
||||
"end": 1597171232854,
|
||||
"type": "ROVER",
|
||||
"color": "fuchsia",
|
||||
"textColor": "black"
|
||||
},
|
||||
{
|
||||
"name": "Activity 4",
|
||||
"start": 1597171132854,
|
||||
"end": 1597171232854,
|
||||
"type": "ROVER",
|
||||
"color": "fuchsia",
|
||||
"textColor": "black"
|
||||
}
|
||||
],
|
||||
"VIPER": [
|
||||
{
|
||||
"name": "Activity 3",
|
||||
"start": 1597170132854,
|
||||
"end": 1597171202854,
|
||||
"type": "VIPER",
|
||||
"color": "fuchsia",
|
||||
"textColor": "black"
|
||||
}
|
||||
]
|
||||
}
|
||||
49
src/plugins/timeline/plugin.js
Normal file
49
src/plugins/timeline/plugin.js
Normal file
@@ -0,0 +1,49 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2020, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
import TimelineViewProvider from './TimelineViewProvider';
|
||||
|
||||
export default function () {
|
||||
return function install(openmct) {
|
||||
openmct.types.addType('plan', {
|
||||
name: 'Plan',
|
||||
key: 'plan',
|
||||
description: 'An activity timeline',
|
||||
creatable: true,
|
||||
cssClass: 'icon-timeline',
|
||||
form: [
|
||||
{
|
||||
name: 'Upload Plan (JSON File)',
|
||||
key: 'selectFile',
|
||||
control: 'file-input',
|
||||
required: true,
|
||||
text: 'Select File',
|
||||
type: 'application/json'
|
||||
}
|
||||
],
|
||||
initialize: function (domainObject) {
|
||||
}
|
||||
});
|
||||
openmct.objectViews.addProvider(new TimelineViewProvider(openmct));
|
||||
};
|
||||
}
|
||||
|
||||
205
src/plugins/timeline/pluginSpec.js
Normal file
205
src/plugins/timeline/pluginSpec.js
Normal file
@@ -0,0 +1,205 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2020, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
import { createOpenMct, resetApplicationState } from "utils/testing";
|
||||
import TimelinePlugin from "./plugin";
|
||||
import Vue from 'vue';
|
||||
import TimelineViewLayout from "./TimelineViewLayout.vue";
|
||||
|
||||
describe('the plugin', function () {
|
||||
let planDefinition;
|
||||
let element;
|
||||
let child;
|
||||
let openmct;
|
||||
|
||||
beforeEach((done) => {
|
||||
const appHolder = document.createElement('div');
|
||||
appHolder.style.width = '640px';
|
||||
appHolder.style.height = '480px';
|
||||
|
||||
openmct = createOpenMct();
|
||||
openmct.install(new TimelinePlugin());
|
||||
|
||||
planDefinition = openmct.types.get('plan').definition;
|
||||
|
||||
element = document.createElement('div');
|
||||
element.style.width = '640px';
|
||||
element.style.height = '480px';
|
||||
child = document.createElement('div');
|
||||
child.style.width = '640px';
|
||||
child.style.height = '480px';
|
||||
element.appendChild(child);
|
||||
|
||||
openmct.time.bounds({
|
||||
start: 1597160002854,
|
||||
end: 1597181232854
|
||||
});
|
||||
|
||||
openmct.on('start', done);
|
||||
openmct.startHeadless(appHolder);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
return resetApplicationState(openmct);
|
||||
});
|
||||
|
||||
let mockPlanObject = {
|
||||
name: 'Plan',
|
||||
key: 'plan',
|
||||
creatable: true
|
||||
};
|
||||
|
||||
it('defines a plan object type with the correct key', () => {
|
||||
expect(planDefinition.key).toEqual(mockPlanObject.key);
|
||||
});
|
||||
|
||||
describe('the plan object', () => {
|
||||
|
||||
it('is creatable', () => {
|
||||
expect(planDefinition.creatable).toEqual(mockPlanObject.creatable);
|
||||
});
|
||||
|
||||
it('provides a timeline view', () => {
|
||||
const testViewObject = {
|
||||
id: "test-object",
|
||||
type: "plan"
|
||||
};
|
||||
|
||||
const applicableViews = openmct.objectViews.get(testViewObject);
|
||||
let timelineView = applicableViews.find((viewProvider) => viewProvider.key === 'timeline.view');
|
||||
expect(timelineView).toBeDefined();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('the timeline view displays activities', () => {
|
||||
let planDomainObject;
|
||||
let component;
|
||||
let planViewComponent;
|
||||
|
||||
beforeEach((done) => {
|
||||
planDomainObject = {
|
||||
type: 'plan',
|
||||
id: "test-object",
|
||||
selectFile: {
|
||||
body: JSON.stringify({
|
||||
"TEST-GROUP": [
|
||||
{
|
||||
"name": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua",
|
||||
"start": 1597170002854,
|
||||
"end": 1597171032854,
|
||||
"type": "TEST-GROUP",
|
||||
"color": "fuchsia",
|
||||
"textColor": "black"
|
||||
},
|
||||
{
|
||||
"name": "Sed ut perspiciatis",
|
||||
"start": 1597171132854,
|
||||
"end": 1597171232854,
|
||||
"type": "TEST-GROUP",
|
||||
"color": "fuchsia",
|
||||
"textColor": "black"
|
||||
}
|
||||
]
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
let viewContainer = document.createElement('div');
|
||||
child.append(viewContainer);
|
||||
component = new Vue({
|
||||
provide: {
|
||||
openmct: openmct,
|
||||
domainObject: planDomainObject
|
||||
},
|
||||
el: viewContainer,
|
||||
components: {
|
||||
TimelineViewLayout
|
||||
},
|
||||
template: '<timeline-view-layout/>'
|
||||
});
|
||||
|
||||
return Vue.nextTick().then(() => {
|
||||
planViewComponent = component.$root.$children[0].$children[0];
|
||||
setTimeout(() => {
|
||||
clearInterval(planViewComponent.resizeTimer);
|
||||
//TODO: this is a hack to ensure the canvas has a width - maybe there's a better way to set the width of the plan div
|
||||
planViewComponent.width = 1200;
|
||||
planViewComponent.setScaleAndPlotActivities();
|
||||
done();
|
||||
}, 300);
|
||||
});
|
||||
});
|
||||
|
||||
it('loads activities into the view', () => {
|
||||
expect(planViewComponent.json).toBeDefined();
|
||||
expect(planViewComponent.json["TEST-GROUP"].length).toEqual(2);
|
||||
});
|
||||
|
||||
it('loads a time axis into the view', () => {
|
||||
let ticks = planViewComponent.axisElement.node().querySelectorAll('g.tick');
|
||||
expect(ticks.length).toEqual(11);
|
||||
});
|
||||
|
||||
it('calculates the activity layout', () => {
|
||||
const expectedActivitiesByRow = {
|
||||
"0": [
|
||||
{
|
||||
"heading": "TEST-GROUP",
|
||||
"activity": {
|
||||
"color": "fuchsia",
|
||||
"textColor": "black"
|
||||
},
|
||||
"textLines": [
|
||||
"Lorem ipsum dolor sit amet, consectetur adipiscing elit, ",
|
||||
"sed sed do eiusmod tempor incididunt ut labore et "
|
||||
],
|
||||
"textStart": -47.51342439943476,
|
||||
"textY": 12,
|
||||
"start": -47.51625058878945,
|
||||
"end": 204.97315120113046,
|
||||
"rectWidth": -4.9971738106453145
|
||||
}
|
||||
],
|
||||
"42": [
|
||||
{
|
||||
"heading": "",
|
||||
"activity": {
|
||||
"color": "fuchsia",
|
||||
"textColor": "black"
|
||||
},
|
||||
"textLines": [
|
||||
"Sed ut perspiciatis "
|
||||
],
|
||||
"textStart": -48.483749411210546,
|
||||
"textY": 54,
|
||||
"start": -52.99858690532266,
|
||||
"end": 9.032501177578908,
|
||||
"rectWidth": -0.48516250588788523
|
||||
}
|
||||
]
|
||||
};
|
||||
expect(Object.keys(planViewComponent.activitiesByRow)).toEqual(Object.keys(expectedActivitiesByRow));
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
57
src/plugins/timeline/timeline-axis.scss
Normal file
57
src/plugins/timeline/timeline-axis.scss
Normal file
@@ -0,0 +1,57 @@
|
||||
.c-timeline {
|
||||
$h: 18px;
|
||||
$tickYPos: ($h / 2) + 12px + 10px;
|
||||
$tickXPos: 100px;
|
||||
|
||||
height: 100%;
|
||||
|
||||
svg {
|
||||
text-rendering: geometricPrecision;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
> g.axis {
|
||||
// Overall Tick holder
|
||||
transform: translateY($tickYPos) translateX($tickXPos);
|
||||
|
||||
g {
|
||||
//Each tick. These move on drag.
|
||||
line {
|
||||
// Line beneath ticks
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
text:not(.activity) {
|
||||
// Tick labels
|
||||
fill: $colorBodyFg;
|
||||
font-size: 1em;
|
||||
paint-order: stroke;
|
||||
font-weight: bold;
|
||||
stroke: $colorBodyBg;
|
||||
stroke-linecap: butt;
|
||||
stroke-linejoin: bevel;
|
||||
stroke-width: 6px;
|
||||
}
|
||||
|
||||
text.activity {
|
||||
stroke: none;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
.nowMarker {
|
||||
width: 2px;
|
||||
position: absolute;
|
||||
z-index: 10;
|
||||
background: gray;
|
||||
|
||||
& .icon-arrow-down {
|
||||
font-size: large;
|
||||
position: absolute;
|
||||
top: -8px;
|
||||
left: -8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -26,6 +26,7 @@
|
||||
@import "../plugins/timeConductor/conductor-mode.scss";
|
||||
@import "../plugins/timeConductor/conductor-mode-icon.scss";
|
||||
@import "../plugins/timeConductor/date-picker.scss";
|
||||
@import "../plugins/timeline/timeline-axis.scss";
|
||||
@import "../ui/components/object-frame.scss";
|
||||
@import "../ui/components/object-label.scss";
|
||||
@import "../ui/components/progress-bar.scss";
|
||||
|
||||
@@ -113,7 +113,7 @@
|
||||
|
||||
<script>
|
||||
import ViewSwitcher from './ViewSwitcher.vue';
|
||||
import NotebookMenuSwitcher from '@/plugins/notebook/components/notebook-menu-switcher.vue';
|
||||
import NotebookMenuSwitcher from '@/plugins/notebook/components/NotebookMenuSwitcher.vue';
|
||||
|
||||
const PLACEHOLDER_OBJECT = {};
|
||||
|
||||
|
||||
@@ -248,20 +248,14 @@
|
||||
}
|
||||
|
||||
// TRANSITIONS
|
||||
.slide-left,
|
||||
.slide-right {
|
||||
animation-duration: 500ms;
|
||||
animation-iteration-count: 1;
|
||||
transition: all;
|
||||
transition-timing-function: ease-in-out;
|
||||
}
|
||||
.children-enter-active {
|
||||
&.down {
|
||||
animation: animSlideLeft 500ms;
|
||||
}
|
||||
|
||||
.slide-left {
|
||||
animation-name: animSlideLeft;
|
||||
}
|
||||
|
||||
.slide-right {
|
||||
animation-name: animSlideRight;
|
||||
&.up {
|
||||
animation: animSlideRight 500ms;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes animSlideLeft {
|
||||
|
||||
@@ -13,8 +13,17 @@
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- search loading -->
|
||||
<li
|
||||
v-if="searchLoading && activeSearch"
|
||||
class="c-tree__item c-tree-and-search__loading loading"
|
||||
>
|
||||
<span class="c-tree__item__label">Searching...</span>
|
||||
</li>
|
||||
|
||||
<!-- no results -->
|
||||
<div
|
||||
v-if="(searchValue && allTreeItems.length === 0 && !isLoading) || (searchValue && searchResultItems.length === 0)"
|
||||
v-if="(searchValue && searchResultItems.length === 0 && !searchLoading)"
|
||||
class="c-tree-and-search__no-results"
|
||||
>
|
||||
No results found
|
||||
@@ -48,14 +57,16 @@
|
||||
</li>
|
||||
<!-- end loading -->
|
||||
</div>
|
||||
|
||||
<!-- currently viewed children -->
|
||||
<transition
|
||||
@enter="childrenIn"
|
||||
name="children"
|
||||
appear
|
||||
>
|
||||
<li
|
||||
v-if="!isLoading"
|
||||
:class="childrenSlideClass"
|
||||
v-if="!isLoading && !searchLoading"
|
||||
:style="childrenListStyles()"
|
||||
:class="childrenSlideClass"
|
||||
>
|
||||
<ul
|
||||
ref="scrollable"
|
||||
@@ -77,7 +88,7 @@
|
||||
@expanded="handleExpanded"
|
||||
/>
|
||||
<li
|
||||
v-if="visibleItems.length === 0 && !noVisibleItems"
|
||||
v-if="visibleItems.length === 0 && !noVisibleItems && !activeSearch"
|
||||
:style="indicatorLeftOffset"
|
||||
class="c-tree__item c-tree__item--empty"
|
||||
>
|
||||
@@ -93,6 +104,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import _ from 'lodash';
|
||||
import treeItem from './tree-item.vue';
|
||||
import search from '../components/search.vue';
|
||||
|
||||
@@ -123,12 +135,13 @@ export default {
|
||||
|
||||
return {
|
||||
isLoading: false,
|
||||
searchLoading: false,
|
||||
searchValue: '',
|
||||
allTreeItems: [],
|
||||
searchResultItems: [],
|
||||
visibleItems: [],
|
||||
ancestors: [],
|
||||
childrenSlideClass: 'slide-left',
|
||||
childrenSlideClass: 'down',
|
||||
availableContainerHeight: 0,
|
||||
noScroll: true,
|
||||
updatingView: false,
|
||||
@@ -142,7 +155,8 @@ export default {
|
||||
settingChildrenHeight: false,
|
||||
isMobile: isMobile.mobileName,
|
||||
multipleRootChildren: false,
|
||||
noVisibleItems: false
|
||||
noVisibleItems: false,
|
||||
observedAncestors: {}
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -231,6 +245,9 @@ export default {
|
||||
if (!this.isLoading) {
|
||||
this.setContainerHeight();
|
||||
}
|
||||
},
|
||||
ancestors() {
|
||||
this.observeAncestors();
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
@@ -267,8 +284,12 @@ export default {
|
||||
this.getAllChildren(rootNode);
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.getSearchResults = _.debounce(this.getSearchResults, 400);
|
||||
},
|
||||
destroyed() {
|
||||
window.removeEventListener('resize', this.handleWindowResize);
|
||||
this.stopObservingAncestors();
|
||||
},
|
||||
methods: {
|
||||
updatevisibleItems() {
|
||||
@@ -312,7 +333,7 @@ export default {
|
||||
async setContainerHeight() {
|
||||
await this.$nextTick();
|
||||
let mainTree = this.$refs.mainTree;
|
||||
let mainTreeHeight = mainTree.clientHeight;
|
||||
let mainTreeHeight = mainTree && mainTree.clientHeight ? mainTree.clientHeight : 0;
|
||||
|
||||
if (mainTreeHeight !== 0) {
|
||||
this.calculateChildHeight(() => {
|
||||
@@ -449,6 +470,46 @@ export default {
|
||||
navigateToParent: navToParent
|
||||
};
|
||||
},
|
||||
observeAncestors() {
|
||||
let observedAncestorIds = Object.keys(this.observedAncestors);
|
||||
|
||||
// observe any ancestors, not currently being observed
|
||||
this.ancestors.forEach((ancestor) => {
|
||||
let ancestorObject = ancestor.object;
|
||||
let ancestorKeyString = this.openmct.objects.makeKeyString(ancestorObject.identifier);
|
||||
let index = observedAncestorIds.indexOf(ancestorKeyString);
|
||||
|
||||
if (index !== -1) { // currently observed
|
||||
observedAncestorIds.splice(index, 1); // remove all active ancestors from id tracking
|
||||
} else { // not observed, observe it
|
||||
this.observeAncestor(ancestorKeyString, ancestorObject);
|
||||
}
|
||||
});
|
||||
|
||||
// remove any ancestors currnetly being observed that are no longer active ancestors
|
||||
this.stopObservingAncestors(observedAncestorIds);
|
||||
},
|
||||
stopObservingAncestors(ids = Object.keys(this.observedAncestors)) {
|
||||
ids.forEach((id) => {
|
||||
this.observedAncestors[id]();
|
||||
this.observedAncestors[id] = undefined;
|
||||
delete this.observedAncestors[id];
|
||||
});
|
||||
},
|
||||
observeAncestor(id, object) {
|
||||
this.observedAncestors[id] = this.openmct.objects.observe(object, 'location',
|
||||
(location) => {
|
||||
let ancestorObjects = this.ancestors.map(ancestor => ancestor.object);
|
||||
// ancestor has been removed from tree, reset to it's parent
|
||||
if (location === null) {
|
||||
let index = ancestorObjects.indexOf(object);
|
||||
let parentIndex = index - 1;
|
||||
if (this.ancestors[parentIndex]) {
|
||||
this.handleReset(this.ancestors[parentIndex]);
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
addChild(child) {
|
||||
let item = this.buildTreeItem(child);
|
||||
this.allTreeItems.push(item);
|
||||
@@ -466,6 +527,7 @@ export default {
|
||||
|
||||
this.autoScroll();
|
||||
this.isLoading = false;
|
||||
this.setContainerHeight();
|
||||
},
|
||||
async jumpToPath(saveExpandedPath = false) {
|
||||
// switching back and forth between multiple root children can cause issues,
|
||||
@@ -551,20 +613,25 @@ export default {
|
||||
navigateToParent
|
||||
};
|
||||
});
|
||||
this.searchLoading = false;
|
||||
},
|
||||
searchTree(value) {
|
||||
this.searchValue = value;
|
||||
this.searchLoading = true;
|
||||
|
||||
if (this.searchValue !== '') {
|
||||
this.getSearchResults();
|
||||
} else {
|
||||
this.searchLoading = false;
|
||||
}
|
||||
},
|
||||
searchActivated() {
|
||||
this.activeSearch = true;
|
||||
this.$refs.scrollable.scrollTop = 0;
|
||||
},
|
||||
searchDeactivated() {
|
||||
async searchDeactivated() {
|
||||
this.activeSearch = false;
|
||||
await this.$nextTick();
|
||||
this.$refs.scrollable.scrollTop = 0;
|
||||
this.setContainerHeight();
|
||||
},
|
||||
@@ -573,7 +640,7 @@ export default {
|
||||
return;
|
||||
}
|
||||
|
||||
this.childrenSlideClass = 'slide-right';
|
||||
this.childrenSlideClass = 'up';
|
||||
this.ancestors.splice(this.ancestors.indexOf(node) + 1);
|
||||
this.getAllChildren(node);
|
||||
this.setCurrentNavigatedPath();
|
||||
@@ -583,7 +650,7 @@ export default {
|
||||
return;
|
||||
}
|
||||
|
||||
this.childrenSlideClass = 'slide-left';
|
||||
this.childrenSlideClass = 'down';
|
||||
let newParent = this.buildTreeItem(node);
|
||||
this.ancestors.push(newParent);
|
||||
this.getAllChildren(newParent);
|
||||
@@ -639,11 +706,6 @@ export default {
|
||||
overflow: this.noScroll ? 'hidden' : 'scroll'
|
||||
};
|
||||
},
|
||||
childrenIn(el, done) {
|
||||
// still needing this timeout for some reason
|
||||
window.setTimeout(this.setContainerHeight, RECHECK_DELAY);
|
||||
done();
|
||||
},
|
||||
getElementStyleValue(el, style) {
|
||||
let styleString = window.getComputedStyle(el)[style];
|
||||
let index = styleString.indexOf('px');
|
||||
|
||||
Reference in New Issue
Block a user