mirror of
https://github.com/nasa/openmct.git
synced 2025-06-25 02:29:24 +00:00
Compare commits
125 Commits
legacy-per
...
openmct-st
Author | SHA1 | Date | |
---|---|---|---|
f04c274d33 | |||
3624236c26 | |||
0126542411 | |||
2c49e62863 | |||
c2df3cdd14 | |||
b0203f2272 | |||
77b720d00d | |||
ba982671b2 | |||
02be8a9875 | |||
3bd57c8fff | |||
5df7d92d64 | |||
a8228406de | |||
2401473012 | |||
94091b25ec | |||
c191ffb37d | |||
e502fb88fa | |||
37a52cb011 | |||
04fb4e8a82 | |||
5f03dc45ee | |||
14ac758760 | |||
eb709a60cb | |||
eba1a48a44 | |||
4a0654dbcb | |||
9b6d339d69 | |||
f90afb9277 | |||
018dfb1e28 | |||
5646a252f7 | |||
c72a02aaa3 | |||
0e6ce7f58b | |||
8cd6a4c6a3 | |||
02fc162197 | |||
84d21a3695 | |||
1a6369c2b9 | |||
463c44679d | |||
c1f3ea4e61 | |||
bf3fd66942 | |||
8414ded1ec | |||
142b767470 | |||
184b716b53 | |||
646c871c76 | |||
e53399495b | |||
d27f73579b | |||
ba401b3341 | |||
5ef02ec4a2 | |||
d788031019 | |||
1ae8199e89 | |||
2deb4e8474 | |||
7f10681424 | |||
c756adad6f | |||
f3d593bc1e | |||
b637307de6 | |||
b6e0208e71 | |||
631876cab3 | |||
a192d46c2b | |||
6923f17645 | |||
d870874649 | |||
711a7a2eb5 | |||
87a45de05b | |||
ab76451360 | |||
c105a08cfe | |||
b87375a809 | |||
9fed056d22 | |||
251bf21933 | |||
a180bf7c02 | |||
ed8a54f0f9 | |||
a91179091f | |||
5f7e34ce6c | |||
ff3c2da0f9 | |||
db33f0538a | |||
28d5821120 | |||
f5ee457274 | |||
9d2770e4d2 | |||
257a8e2e2d | |||
8b25009816 | |||
074fe4481a | |||
fbd928b842 | |||
110947db09 | |||
baa8078d23 | |||
ee60013f45 | |||
ef91e92fbc | |||
d201cac4ac | |||
dcb3ccfec7 | |||
505796d9f0 | |||
56120ba1bb | |||
225b235059 | |||
de614ff606 | |||
78522cd4f1 | |||
ca232d45cc | |||
df495c841a | |||
92a37ef36b | |||
fd731ca430 | |||
263b1cd3d5 | |||
978fc8b5a3 | |||
698ccc5a35 | |||
e5aa5b5a5f | |||
b942988ef8 | |||
1eec20f2ea | |||
767a2048eb | |||
e65cf1661c | |||
0eae48646c | |||
0ba8a275d2 | |||
d8d32cc3ac | |||
a800848fe1 | |||
6881d98ba6 | |||
48d077cd2e | |||
030dd93c91 | |||
03bf6fc0a3 | |||
ef0a2ed5d2 | |||
a40aa84752 | |||
d3b69dda82 | |||
d3126ebf5c | |||
4479cbc7a2 | |||
f8ff44dac0 | |||
8f4280d15b | |||
6daa27ff31 | |||
43f6c3f85d | |||
1a7c76cf3e | |||
cee9cd7bd1 | |||
c42df20281 | |||
b4149bd2b3 | |||
f436ac9ba0 | |||
8493b481dd | |||
28723b59b7 | |||
9fa7de0b77 | |||
54bfc84ada |
@ -76,6 +76,7 @@ define([
|
||||
|
||||
workerRequest[prop] = Number(workerRequest[prop]);
|
||||
});
|
||||
|
||||
workerRequest.name = domainObject.name;
|
||||
|
||||
return workerRequest;
|
||||
|
@ -108,7 +108,6 @@
|
||||
|
||||
for (; nextStep < end && data.length < 5000; nextStep += step) {
|
||||
data.push({
|
||||
name: request.name,
|
||||
utc: nextStep,
|
||||
yesterday: nextStep - 60 * 60 * 24 * 1000,
|
||||
sin: sin(nextStep, period, amplitude, offset, phase, randomness),
|
||||
|
@ -27,7 +27,7 @@ define([
|
||||
) {
|
||||
function ImageryPlugin() {
|
||||
|
||||
var IMAGE_SAMPLES = [
|
||||
const IMAGE_SAMPLES = [
|
||||
"https://www.hq.nasa.gov/alsj/a16/AS16-117-18731.jpg",
|
||||
"https://www.hq.nasa.gov/alsj/a16/AS16-117-18732.jpg",
|
||||
"https://www.hq.nasa.gov/alsj/a16/AS16-117-18733.jpg",
|
||||
@ -47,13 +47,14 @@ define([
|
||||
"https://www.hq.nasa.gov/alsj/a16/AS16-117-18747.jpg",
|
||||
"https://www.hq.nasa.gov/alsj/a16/AS16-117-18748.jpg"
|
||||
];
|
||||
const IMAGE_DELAY = 20000;
|
||||
|
||||
function pointForTimestamp(timestamp, name) {
|
||||
return {
|
||||
name: name,
|
||||
utc: Math.floor(timestamp / 5000) * 5000,
|
||||
local: Math.floor(timestamp / 5000) * 5000,
|
||||
url: IMAGE_SAMPLES[Math.floor(timestamp / 5000) % IMAGE_SAMPLES.length]
|
||||
utc: Math.floor(timestamp / IMAGE_DELAY) * IMAGE_DELAY,
|
||||
local: Math.floor(timestamp / IMAGE_DELAY) * IMAGE_DELAY,
|
||||
url: IMAGE_SAMPLES[Math.floor(timestamp / IMAGE_DELAY) % IMAGE_SAMPLES.length]
|
||||
};
|
||||
}
|
||||
|
||||
@ -64,7 +65,7 @@ define([
|
||||
subscribe: function (domainObject, callback) {
|
||||
var interval = setInterval(function () {
|
||||
callback(pointForTimestamp(Date.now(), domainObject.name));
|
||||
}, 5000);
|
||||
}, IMAGE_DELAY);
|
||||
|
||||
return function () {
|
||||
clearInterval(interval);
|
||||
@ -81,9 +82,9 @@ define([
|
||||
var start = options.start;
|
||||
var end = Math.min(options.end, Date.now());
|
||||
var data = [];
|
||||
while (start <= end && data.length < 5000) {
|
||||
while (start <= end && data.length < IMAGE_DELAY) {
|
||||
data.push(pointForTimestamp(start, domainObject.name));
|
||||
start += 5000;
|
||||
start += IMAGE_DELAY;
|
||||
}
|
||||
|
||||
return Promise.resolve(data);
|
||||
|
88
index.html
88
index.html
@ -30,12 +30,50 @@
|
||||
<link rel="icon" type="image/png" href="dist/favicons/favicon-96x96.png" sizes="96x96" type="image/x-icon">
|
||||
<link rel="icon" type="image/png" href="dist/favicons/favicon-32x32.png" sizes="32x32" type="image/x-icon">
|
||||
<link rel="icon" type="image/png" href="dist/favicons/favicon-16x16.png" sizes="16x16" type="image/x-icon">
|
||||
<style type="text/css">
|
||||
@keyframes splash-spinner {
|
||||
0% {
|
||||
transform: translate(-50%, -50%) rotate(0deg); }
|
||||
100% {
|
||||
transform: translate(-50%, -50%) rotate(360deg); } }
|
||||
|
||||
#splash-screen {
|
||||
background-color: black;
|
||||
position: absolute;
|
||||
top: 0; right: 0; bottom: 0; left: 0;
|
||||
z-index: 10000;
|
||||
}
|
||||
|
||||
#splash-screen:before {
|
||||
animation-name: splash-spinner;
|
||||
animation-duration: 0.5s;
|
||||
animation-iteration-count: infinite;
|
||||
animation-timing-function: linear;
|
||||
border-radius: 50%;
|
||||
border-color: rgba(255,255,255,0.25);
|
||||
border-top-color: white;
|
||||
border-style: solid;
|
||||
border-width: 10px;
|
||||
content: '';
|
||||
display: block;
|
||||
opacity: 0.25;
|
||||
position: absolute;
|
||||
left: 50%; top: 50%;
|
||||
height: 100px; width: 100px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
</body>
|
||||
<script>
|
||||
const THIRTY_SECONDS = 30 * 1000;
|
||||
const THIRTY_MINUTES = THIRTY_SECONDS * 60;
|
||||
const ONE_MINUTE = THIRTY_SECONDS * 2;
|
||||
const FIVE_MINUTES = ONE_MINUTE * 5;
|
||||
const FIFTEEN_MINUTES = FIVE_MINUTES * 3;
|
||||
const THIRTY_MINUTES = FIFTEEN_MINUTES * 2;
|
||||
const ONE_HOUR = THIRTY_MINUTES * 2;
|
||||
const TWO_HOURS = ONE_HOUR * 2;
|
||||
const ONE_DAY = ONE_HOUR * 24;
|
||||
|
||||
[
|
||||
'example/eventGenerator'
|
||||
@ -48,6 +86,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"
|
||||
@ -72,21 +111,21 @@
|
||||
{
|
||||
label: 'Last Day',
|
||||
bounds: {
|
||||
start: () => Date.now() - 1000 * 60 * 60 * 24,
|
||||
start: () => Date.now() - ONE_DAY,
|
||||
end: () => Date.now()
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'Last 2 hours',
|
||||
bounds: {
|
||||
start: () => Date.now() - 1000 * 60 * 60 * 2,
|
||||
start: () => Date.now() - TWO_HOURS,
|
||||
end: () => Date.now()
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'Last hour',
|
||||
bounds: {
|
||||
start: () => Date.now() - 1000 * 60 * 60,
|
||||
start: () => Date.now() - ONE_HOUR,
|
||||
end: () => Date.now()
|
||||
}
|
||||
}
|
||||
@ -95,7 +134,7 @@
|
||||
records: 10,
|
||||
// maximum duration between start and end bounds
|
||||
// for utc-based time systems this is in milliseconds
|
||||
limit: 1000 * 60 * 60 * 24
|
||||
limit: ONE_DAY
|
||||
},
|
||||
{
|
||||
name: "Realtime",
|
||||
@ -104,7 +143,44 @@
|
||||
clockOffsets: {
|
||||
start: - THIRTY_MINUTES,
|
||||
end: THIRTY_SECONDS
|
||||
}
|
||||
},
|
||||
presets: [
|
||||
{
|
||||
label: '1 Hour',
|
||||
bounds: {
|
||||
start: - ONE_HOUR,
|
||||
end: THIRTY_SECONDS
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '30 Minutes',
|
||||
bounds: {
|
||||
start: - THIRTY_MINUTES,
|
||||
end: THIRTY_SECONDS
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '15 Minutes',
|
||||
bounds: {
|
||||
start: - FIFTEEN_MINUTES,
|
||||
end: THIRTY_SECONDS
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '5 Minutes',
|
||||
bounds: {
|
||||
start: - FIVE_MINUTES,
|
||||
end: THIRTY_SECONDS
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '1 Minute',
|
||||
bounds: {
|
||||
start: - ONE_MINUTE,
|
||||
end: THIRTY_SECONDS
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}));
|
||||
|
@ -86,7 +86,7 @@ module.exports = (config) => {
|
||||
reports: ['html', 'lcovonly', 'text-summary'],
|
||||
thresholds: {
|
||||
global: {
|
||||
lines: 64
|
||||
lines: 65
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "openmct",
|
||||
"version": "1.3.0-SNAPSHOT",
|
||||
"version": "1.4.1-SNAPSHOT",
|
||||
"description": "The Open MCT core platform",
|
||||
"dependencies": {},
|
||||
"devDependencies": {
|
||||
|
@ -143,8 +143,8 @@ define([
|
||||
"$window"
|
||||
],
|
||||
"group": "windowing",
|
||||
"cssClass": "icon-new-window",
|
||||
"priority": "preferred"
|
||||
"priority": 10,
|
||||
"cssClass": "icon-new-window"
|
||||
}
|
||||
],
|
||||
"runs": [
|
||||
|
@ -139,7 +139,9 @@ define([
|
||||
],
|
||||
"description": "Edit",
|
||||
"category": "view-control",
|
||||
"cssClass": "major icon-pencil"
|
||||
"cssClass": "major icon-pencil",
|
||||
"group": "action",
|
||||
"priority": 10
|
||||
},
|
||||
{
|
||||
"key": "properties",
|
||||
@ -150,6 +152,8 @@ define([
|
||||
"implementation": PropertiesAction,
|
||||
"cssClass": "major icon-pencil",
|
||||
"name": "Edit Properties...",
|
||||
"group": "action",
|
||||
"priority": 10,
|
||||
"description": "Edit properties of this object.",
|
||||
"depends": [
|
||||
"dialogService"
|
||||
|
@ -20,12 +20,12 @@
|
||||
at runtime from the About dialog for additional information.
|
||||
-->
|
||||
<div class="c-object-label"
|
||||
ng-class="{ 'is-missing': model.status === 'missing' }"
|
||||
ng-class="{ 'is-status--missing': model.status === 'missing' }"
|
||||
>
|
||||
<div class="c-object-label__type-icon {{type.getCssClass()}}"
|
||||
ng-class="{ 'l-icon-link':location.isLink() }"
|
||||
>
|
||||
<span class="is-missing__indicator" title="This item is missing"></span>
|
||||
<span class="is-status__indicator" title="This item is missing or suspect"></span>
|
||||
</div>
|
||||
<div class='c-object-label__name'>{{model.name}}</div>
|
||||
</div>
|
||||
|
@ -114,7 +114,12 @@ define(["objectUtils"],
|
||||
var self = this,
|
||||
domainObject = this.domainObject;
|
||||
|
||||
let newStyleObject = objectUtils.toNewFormat(domainObject.getModel(), domainObject.getId());
|
||||
const identifier = {
|
||||
namespace: this.getSpace(),
|
||||
key: this.getKey()
|
||||
};
|
||||
|
||||
let newStyleObject = objectUtils.toNewFormat(domainObject.getModel(), identifier);
|
||||
|
||||
return this.openmct.objects
|
||||
.save(newStyleObject)
|
||||
@ -146,6 +151,7 @@ define(["objectUtils"],
|
||||
return domainObject.useCapability("mutation", function () {
|
||||
return model;
|
||||
}, modified);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -99,8 +99,8 @@ define(
|
||||
|
||||
mockNewStyleDomainObject = Object.assign({}, model);
|
||||
mockNewStyleDomainObject.identifier = {
|
||||
namespace: "",
|
||||
key: id
|
||||
namespace: SPACE,
|
||||
key: key
|
||||
};
|
||||
|
||||
// Simulate mutation capability
|
||||
|
@ -66,6 +66,8 @@ define([
|
||||
"description": "Move object to another location.",
|
||||
"cssClass": "icon-move",
|
||||
"category": "contextual",
|
||||
"group": "action",
|
||||
"priority": 9,
|
||||
"implementation": MoveAction,
|
||||
"depends": [
|
||||
"policyService",
|
||||
@ -79,6 +81,8 @@ define([
|
||||
"description": "Duplicate object to another location.",
|
||||
"cssClass": "icon-duplicate",
|
||||
"category": "contextual",
|
||||
"group": "action",
|
||||
"priority": 8,
|
||||
"implementation": CopyAction,
|
||||
"depends": [
|
||||
"$log",
|
||||
@ -95,6 +99,8 @@ define([
|
||||
"description": "Create Link to object in another location.",
|
||||
"cssClass": "icon-link",
|
||||
"category": "contextual",
|
||||
"group": "action",
|
||||
"priority": 7,
|
||||
"implementation": LinkAction,
|
||||
"depends": [
|
||||
"policyService",
|
||||
|
@ -19,7 +19,7 @@
|
||||
this source code distribution or the Licensing information page available
|
||||
at runtime from the About dialog for additional information.
|
||||
-->
|
||||
<div class="c-clock l-time-display" ng-controller="ClockController as clock">
|
||||
<div class="c-clock l-time-display u-style-receiver js-style-receiver" ng-controller="ClockController as clock">
|
||||
<div class="c-clock__timezone">
|
||||
{{clock.zone()}}
|
||||
</div>
|
||||
|
@ -19,7 +19,7 @@
|
||||
this source code distribution or the Licensing information page available
|
||||
at runtime from the About dialog for additional information.
|
||||
-->
|
||||
<div class="c-timer is-{{timer.timerState}}" ng-controller="TimerController as timer">
|
||||
<div class="c-timer u-style-receiver js-style-receiver is-{{timer.timerState}}" ng-controller="TimerController as timer">
|
||||
<div class="c-timer__controls">
|
||||
<button ng-click="timer.clickStopButton()"
|
||||
ng-hide="timer.timerState == 'stopped'"
|
||||
|
@ -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;
|
||||
}
|
||||
);
|
@ -47,6 +47,8 @@ define([
|
||||
"implementation": ExportAsJSONAction,
|
||||
"category": "contextual",
|
||||
"cssClass": "icon-export",
|
||||
"group": "json",
|
||||
"priority": 2,
|
||||
"depends": [
|
||||
"openmct",
|
||||
"exportService",
|
||||
@ -61,6 +63,8 @@ define([
|
||||
"implementation": ImportAsJSONAction,
|
||||
"category": "contextual",
|
||||
"cssClass": "icon-import",
|
||||
"group": "json",
|
||||
"priority": 2,
|
||||
"depends": [
|
||||
"exportService",
|
||||
"identifierService",
|
||||
|
@ -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
|
||||
|
@ -242,7 +242,11 @@ define([
|
||||
|
||||
this.overlays = new OverlayAPI.default();
|
||||
|
||||
this.contextMenu = new api.ContextMenuRegistry();
|
||||
this.menus = new api.MenuAPI(this);
|
||||
|
||||
this.actions = new api.ActionsAPI(this);
|
||||
|
||||
this.status = new api.StatusAPI(this);
|
||||
|
||||
this.router = new ApplicationRouter();
|
||||
|
||||
@ -271,6 +275,7 @@ define([
|
||||
this.install(this.plugins.URLTimeSettingsSynchronizer());
|
||||
this.install(this.plugins.NotificationIndicator());
|
||||
this.install(this.plugins.NewFolderAction());
|
||||
this.install(this.plugins.ViewDatumAction());
|
||||
}
|
||||
|
||||
MCT.prototype = Object.create(EventEmitter.prototype);
|
||||
|
@ -35,5 +35,5 @@ export default function LegacyActionAdapter(openmct, legacyActions) {
|
||||
|
||||
legacyActions.filter(contextualCategoryOnly)
|
||||
.map(LegacyAction => new LegacyContextMenuAction(openmct, LegacyAction))
|
||||
.forEach(openmct.contextMenu.registerAction);
|
||||
.forEach(openmct.actions.register);
|
||||
}
|
||||
|
@ -31,6 +31,8 @@ export default class LegacyContextMenuAction {
|
||||
this.description = LegacyAction.definition.description;
|
||||
this.cssClass = LegacyAction.definition.cssClass;
|
||||
this.LegacyAction = LegacyAction;
|
||||
this.group = LegacyAction.definition.group;
|
||||
this.priority = LegacyAction.definition.priority;
|
||||
}
|
||||
|
||||
invoke(objectPath) {
|
||||
|
@ -128,7 +128,7 @@ define([
|
||||
};
|
||||
|
||||
ObjectServiceProvider.prototype.get = function (key) {
|
||||
const keyString = utils.makeKeyString(key);
|
||||
let keyString = utils.makeKeyString(key);
|
||||
|
||||
return this.objectService.getObjects([keyString])
|
||||
.then(function (results) {
|
||||
|
@ -20,7 +20,7 @@
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
import objectUtils from 'objectUtils';
|
||||
import utils from 'objectUtils';
|
||||
|
||||
export default class LegacyPersistenceAdapter {
|
||||
constructor(openmct) {
|
||||
@ -35,13 +35,43 @@ export default class LegacyPersistenceAdapter {
|
||||
return Promise.resolve(Object.keys(this.openmct.objects.providers));
|
||||
}
|
||||
|
||||
updateObject(legacyDomainObject) {
|
||||
return this.openmct.objects.save(legacyDomainObject.useCapability('adapter'));
|
||||
createObject(space, key, legacyDomainObject) {
|
||||
let object = utils.toNewFormat(legacyDomainObject, {
|
||||
namespace: space,
|
||||
key: key
|
||||
});
|
||||
|
||||
return this.openmct.objects.save(object);
|
||||
}
|
||||
|
||||
readObject(keystring) {
|
||||
let identifier = objectUtils.parseKeyString(keystring);
|
||||
deleteObject(space, key) {
|
||||
const identifier = {
|
||||
namespace: space,
|
||||
key: key
|
||||
};
|
||||
|
||||
return this.openmct.legacyObject(this.openmct.objects.get(identifier));
|
||||
return this.openmct.objects.delete(identifier);
|
||||
}
|
||||
|
||||
updateObject(space, key, legacyDomainObject) {
|
||||
let object = utils.toNewFormat(legacyDomainObject, {
|
||||
namespace: space,
|
||||
key: key
|
||||
});
|
||||
|
||||
return this.openmct.objects.save(object);
|
||||
}
|
||||
|
||||
readObject(space, key) {
|
||||
const identifier = {
|
||||
namespace: space,
|
||||
key: key
|
||||
};
|
||||
|
||||
return this.openmct.objects.get(identifier).then(domainObject => {
|
||||
let object = this.openmct.legacyObject(domainObject);
|
||||
|
||||
return object.model;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
178
src/api/actions/ActionCollection.js
Normal file
178
src/api/actions/ActionCollection.js
Normal file
@ -0,0 +1,178 @@
|
||||
/*****************************************************************************
|
||||
* 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 EventEmitter from 'EventEmitter';
|
||||
import _ from 'lodash';
|
||||
|
||||
class ActionCollection extends EventEmitter {
|
||||
constructor(applicableActions, objectPath, view, openmct) {
|
||||
super();
|
||||
|
||||
this.applicableActions = applicableActions;
|
||||
this.openmct = openmct;
|
||||
this.objectPath = objectPath;
|
||||
this.view = view;
|
||||
this.objectUnsubscribes = [];
|
||||
|
||||
let debounceOptions = {
|
||||
leading: false,
|
||||
trailing: true
|
||||
};
|
||||
|
||||
this._updateActions = _.debounce(this._updateActions.bind(this), 150, debounceOptions);
|
||||
this._update = _.debounce(this._update.bind(this), 150, debounceOptions);
|
||||
|
||||
this._observeObjectPath();
|
||||
this._initializeActions();
|
||||
|
||||
this.openmct.editor.on('isEditing', this._updateActions);
|
||||
}
|
||||
|
||||
disable(actionKeys) {
|
||||
actionKeys.forEach(actionKey => {
|
||||
if (this.applicableActions[actionKey]) {
|
||||
this.applicableActions[actionKey].isDisabled = true;
|
||||
}
|
||||
});
|
||||
this._update();
|
||||
}
|
||||
|
||||
enable(actionKeys) {
|
||||
actionKeys.forEach(actionKey => {
|
||||
if (this.applicableActions[actionKey]) {
|
||||
this.applicableActions[actionKey].isDisabled = false;
|
||||
}
|
||||
});
|
||||
this._update();
|
||||
}
|
||||
|
||||
hide(actionKeys) {
|
||||
actionKeys.forEach(actionKey => {
|
||||
if (this.applicableActions[actionKey]) {
|
||||
this.applicableActions[actionKey].isHidden = true;
|
||||
}
|
||||
});
|
||||
this._update();
|
||||
}
|
||||
|
||||
show(actionKeys) {
|
||||
actionKeys.forEach(actionKey => {
|
||||
if (this.applicableActions[actionKey]) {
|
||||
this.applicableActions[actionKey].isHidden = false;
|
||||
}
|
||||
});
|
||||
this._update();
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.objectUnsubscribes.forEach(unsubscribe => {
|
||||
unsubscribe();
|
||||
});
|
||||
|
||||
this.openmct.editor.off('isEditing', this._updateActions);
|
||||
|
||||
this.emit('destroy', this.view);
|
||||
}
|
||||
|
||||
getVisibleActions() {
|
||||
let actionsArray = Object.keys(this.applicableActions);
|
||||
let visibleActions = [];
|
||||
|
||||
actionsArray.forEach(actionKey => {
|
||||
let action = this.applicableActions[actionKey];
|
||||
|
||||
if (!action.isHidden) {
|
||||
visibleActions.push(action);
|
||||
}
|
||||
});
|
||||
|
||||
return visibleActions;
|
||||
}
|
||||
|
||||
getStatusBarActions() {
|
||||
let actionsArray = Object.keys(this.applicableActions);
|
||||
let statusBarActions = [];
|
||||
|
||||
actionsArray.forEach(actionKey => {
|
||||
let action = this.applicableActions[actionKey];
|
||||
|
||||
if (action.showInStatusBar && !action.isDisabled && !action.isHidden) {
|
||||
statusBarActions.push(action);
|
||||
}
|
||||
});
|
||||
|
||||
return statusBarActions;
|
||||
}
|
||||
|
||||
_update() {
|
||||
this.emit('update', this.applicableActions);
|
||||
}
|
||||
|
||||
_observeObjectPath() {
|
||||
let actionCollection = this;
|
||||
|
||||
function updateObject(oldObject, newObject) {
|
||||
Object.assign(oldObject, newObject);
|
||||
|
||||
actionCollection._updateActions();
|
||||
}
|
||||
|
||||
this.objectPath.forEach(object => {
|
||||
if (object) {
|
||||
let unsubscribe = this.openmct.objects.observe(object, '*', updateObject.bind(this, object));
|
||||
|
||||
this.objectUnsubscribes.push(unsubscribe);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_initializeActions() {
|
||||
Object.keys(this.applicableActions).forEach(key => {
|
||||
this.applicableActions[key].callBack = () => {
|
||||
return this.applicableActions[key].invoke(this.objectPath, this.view);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
_updateActions() {
|
||||
let newApplicableActions = this.openmct.actions._applicableActions(this.objectPath, this.view);
|
||||
|
||||
this.applicableActions = this._mergeOldAndNewActions(this.applicableActions, newApplicableActions);
|
||||
this._initializeActions();
|
||||
this._update();
|
||||
}
|
||||
|
||||
_mergeOldAndNewActions(oldActions, newActions) {
|
||||
let mergedActions = {};
|
||||
Object.keys(newActions).forEach(key => {
|
||||
if (oldActions[key]) {
|
||||
mergedActions[key] = oldActions[key];
|
||||
} else {
|
||||
mergedActions[key] = newActions[key];
|
||||
}
|
||||
});
|
||||
|
||||
return mergedActions;
|
||||
}
|
||||
}
|
||||
|
||||
export default ActionCollection;
|
145
src/api/actions/ActionsAPI.js
Normal file
145
src/api/actions/ActionsAPI.js
Normal file
@ -0,0 +1,145 @@
|
||||
/*****************************************************************************
|
||||
* 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 EventEmitter from 'EventEmitter';
|
||||
import ActionCollection from './ActionCollection';
|
||||
import _ from 'lodash';
|
||||
|
||||
class ActionsAPI extends EventEmitter {
|
||||
constructor(openmct) {
|
||||
super();
|
||||
|
||||
this._allActions = {};
|
||||
this._actionCollections = new WeakMap();
|
||||
this._openmct = openmct;
|
||||
|
||||
this._groupOrder = ['windowing', 'undefined', 'view', 'action', 'json'];
|
||||
|
||||
this.register = this.register.bind(this);
|
||||
this.get = this.get.bind(this);
|
||||
this._applicableActions = this._applicableActions.bind(this);
|
||||
this._updateCachedActionCollections = this._updateCachedActionCollections.bind(this);
|
||||
}
|
||||
|
||||
register(actionDefinition) {
|
||||
this._allActions[actionDefinition.key] = actionDefinition;
|
||||
}
|
||||
|
||||
get(objectPath, view) {
|
||||
let viewContext = view && view.getViewContext && view.getViewContext() || {};
|
||||
|
||||
if (view && !viewContext.skipCache) {
|
||||
let cachedActionCollection = this._actionCollections.get(view);
|
||||
|
||||
if (cachedActionCollection) {
|
||||
return cachedActionCollection;
|
||||
} else {
|
||||
let applicableActions = this._applicableActions(objectPath, view);
|
||||
let actionCollection = new ActionCollection(applicableActions, objectPath, view, this._openmct);
|
||||
|
||||
this._actionCollections.set(view, actionCollection);
|
||||
actionCollection.on('destroy', this._updateCachedActionCollections);
|
||||
|
||||
return actionCollection;
|
||||
}
|
||||
} else {
|
||||
let applicableActions = this._applicableActions(objectPath, view);
|
||||
|
||||
Object.keys(applicableActions).forEach(key => {
|
||||
let action = applicableActions[key];
|
||||
|
||||
action.callBack = () => {
|
||||
return action.invoke(objectPath, view);
|
||||
};
|
||||
});
|
||||
|
||||
return applicableActions;
|
||||
}
|
||||
}
|
||||
|
||||
updateGroupOrder(groupArray) {
|
||||
this._groupOrder = groupArray;
|
||||
}
|
||||
|
||||
_updateCachedActionCollections(key) {
|
||||
if (this._actionCollections.has(key)) {
|
||||
let actionCollection = this._actionCollections.get(key);
|
||||
actionCollection.off('destroy', this._updateCachedActionCollections);
|
||||
|
||||
this._actionCollections.delete(key);
|
||||
}
|
||||
}
|
||||
|
||||
_applicableActions(objectPath, view) {
|
||||
let actionsObject = {};
|
||||
|
||||
let keys = Object.keys(this._allActions).filter(key => {
|
||||
let actionDefinition = this._allActions[key];
|
||||
|
||||
if (actionDefinition.appliesTo === undefined) {
|
||||
return true;
|
||||
} else {
|
||||
return actionDefinition.appliesTo(objectPath, view);
|
||||
}
|
||||
});
|
||||
|
||||
keys.forEach(key => {
|
||||
let action = _.clone(this._allActions[key]);
|
||||
|
||||
actionsObject[key] = action;
|
||||
});
|
||||
|
||||
return actionsObject;
|
||||
}
|
||||
|
||||
_groupAndSortActions(actionsArray) {
|
||||
if (!Array.isArray(actionsArray) && typeof actionsArray === 'object') {
|
||||
actionsArray = Object.keys(actionsArray).map(key => actionsArray[key]);
|
||||
}
|
||||
|
||||
let actionsObject = {};
|
||||
let groupedSortedActionsArray = [];
|
||||
|
||||
function sortDescending(a, b) {
|
||||
return b.priority - a.priority;
|
||||
}
|
||||
|
||||
actionsArray.forEach(action => {
|
||||
if (actionsObject[action.group] === undefined) {
|
||||
actionsObject[action.group] = [action];
|
||||
} else {
|
||||
actionsObject[action.group].push(action);
|
||||
}
|
||||
});
|
||||
|
||||
this._groupOrder.forEach(group => {
|
||||
let groupArray = actionsObject[group];
|
||||
|
||||
if (groupArray) {
|
||||
groupedSortedActionsArray.push(groupArray.sort(sortDescending));
|
||||
}
|
||||
});
|
||||
|
||||
return groupedSortedActionsArray;
|
||||
}
|
||||
}
|
||||
|
||||
export default ActionsAPI;
|
119
src/api/actions/ActionsAPISpec.js
Normal file
119
src/api/actions/ActionsAPISpec.js
Normal file
@ -0,0 +1,119 @@
|
||||
/*****************************************************************************
|
||||
* 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 ActionsAPI from './ActionsAPI';
|
||||
import { createOpenMct, resetApplicationState } from '../../utils/testing';
|
||||
|
||||
describe('The Actions API', () => {
|
||||
let openmct;
|
||||
let actionsAPI;
|
||||
let mockAction;
|
||||
let mockObjectPath;
|
||||
let mockViewContext1;
|
||||
|
||||
beforeEach(() => {
|
||||
openmct = createOpenMct();
|
||||
actionsAPI = new ActionsAPI(openmct);
|
||||
mockAction = {
|
||||
name: 'Test Action',
|
||||
key: 'test-action',
|
||||
cssClass: 'test-action',
|
||||
description: 'This is a test action',
|
||||
group: 'action',
|
||||
priority: 9,
|
||||
appliesTo: (objectPath, view = {}) => {
|
||||
if (view.getViewContext) {
|
||||
let viewContext = view.getViewContext();
|
||||
|
||||
return viewContext.onlyAppliesToTestCase;
|
||||
} else if (objectPath.length) {
|
||||
return objectPath[0].type === 'fake-folder';
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
invoke: () => {
|
||||
}
|
||||
};
|
||||
mockObjectPath = [
|
||||
{
|
||||
name: 'mock folder',
|
||||
type: 'fake-folder',
|
||||
identifier: {
|
||||
key: 'mock-folder',
|
||||
namespace: ''
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'mock parent folder',
|
||||
type: 'fake-folder',
|
||||
identifier: {
|
||||
key: 'mock-parent-folder',
|
||||
namespace: ''
|
||||
}
|
||||
}
|
||||
];
|
||||
mockViewContext1 = {
|
||||
getViewContext: () => {
|
||||
return {
|
||||
onlyAppliesToTestCase: true,
|
||||
skipCache: true
|
||||
};
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
resetApplicationState(openmct);
|
||||
});
|
||||
|
||||
describe("register method", () => {
|
||||
it("adds action to ActionsAPI", () => {
|
||||
actionsAPI.register(mockAction);
|
||||
|
||||
let action = actionsAPI.get(mockObjectPath, mockViewContext1)[mockAction.key];
|
||||
|
||||
expect(action.key).toEqual(mockAction.key);
|
||||
expect(action.name).toEqual(mockAction.name);
|
||||
});
|
||||
});
|
||||
|
||||
describe("get method", () => {
|
||||
beforeEach(() => {
|
||||
actionsAPI.register(mockAction);
|
||||
});
|
||||
|
||||
it("returns an object with relevant actions when invoked with objectPath only", () => {
|
||||
let action = actionsAPI.get(mockObjectPath, mockViewContext1)[mockAction.key];
|
||||
|
||||
expect(action.key).toEqual(mockAction.key);
|
||||
expect(action.name).toEqual(mockAction.name);
|
||||
});
|
||||
|
||||
it("returns an object with relevant actions when invoked with viewContext and skipCache", () => {
|
||||
let action = actionsAPI.get(mockObjectPath, mockViewContext1)[mockAction.key];
|
||||
|
||||
expect(action.key).toEqual(mockAction.key);
|
||||
expect(action.name).toEqual(mockAction.name);
|
||||
});
|
||||
});
|
||||
});
|
@ -28,9 +28,10 @@ define([
|
||||
'./telemetry/TelemetryAPI',
|
||||
'./indicators/IndicatorAPI',
|
||||
'./notifications/NotificationAPI',
|
||||
'./contextMenu/ContextMenuAPI',
|
||||
'./Editor'
|
||||
|
||||
'./Editor',
|
||||
'./menu/MenuAPI',
|
||||
'./actions/ActionsAPI',
|
||||
'./status/StatusAPI'
|
||||
], function (
|
||||
TimeAPI,
|
||||
ObjectAPI,
|
||||
@ -39,8 +40,10 @@ define([
|
||||
TelemetryAPI,
|
||||
IndicatorAPI,
|
||||
NotificationAPI,
|
||||
ContextMenuAPI,
|
||||
EditorAPI
|
||||
EditorAPI,
|
||||
MenuAPI,
|
||||
ActionsAPI,
|
||||
StatusAPI
|
||||
) {
|
||||
return {
|
||||
TimeAPI: TimeAPI,
|
||||
@ -51,6 +54,8 @@ define([
|
||||
IndicatorAPI: IndicatorAPI,
|
||||
NotificationAPI: NotificationAPI.default,
|
||||
EditorAPI: EditorAPI,
|
||||
ContextMenuRegistry: ContextMenuAPI.default
|
||||
MenuAPI: MenuAPI.default,
|
||||
ActionsAPI: ActionsAPI.default,
|
||||
StatusAPI: StatusAPI.default
|
||||
};
|
||||
});
|
||||
|
@ -1,24 +0,0 @@
|
||||
<template>
|
||||
<div class="c-menu">
|
||||
<ul>
|
||||
<li
|
||||
v-for="action in actions"
|
||||
:key="action.name"
|
||||
:class="action.cssClass"
|
||||
:title="action.description"
|
||||
@click="action.invoke(objectPath)"
|
||||
>
|
||||
{{ action.name }}
|
||||
</li>
|
||||
<li v-if="actions.length === 0">
|
||||
No actions defined.
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
inject: ['actions', 'objectPath']
|
||||
};
|
||||
</script>
|
@ -1,159 +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.
|
||||
*****************************************************************************/
|
||||
|
||||
import ContextMenuComponent from './ContextMenu.vue';
|
||||
import Vue from 'vue';
|
||||
|
||||
/**
|
||||
* The ContextMenuAPI allows the addition of new context menu actions, and for the context menu to be launched from
|
||||
* custom HTML elements.
|
||||
* @interface ContextMenuAPI
|
||||
* @memberof module:openmct
|
||||
*/
|
||||
class ContextMenuAPI {
|
||||
constructor() {
|
||||
this._allActions = [];
|
||||
this._activeContextMenu = undefined;
|
||||
|
||||
this._hideActiveContextMenu = this._hideActiveContextMenu.bind(this);
|
||||
this.registerAction = this.registerAction.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines an item to be added to context menus. Allows specification of text, appearance, and behavior when
|
||||
* selected. Applicabilioty can be restricted by specification of an `appliesTo` function.
|
||||
*
|
||||
* @interface ContextMenuAction
|
||||
* @memberof module:openmct
|
||||
* @property {string} name the human-readable name of this view
|
||||
* @property {string} description a longer-form description (typically
|
||||
* a single sentence or short paragraph) of this kind of view
|
||||
* @property {string} cssClass the CSS class to apply to labels for this
|
||||
* view (to add icons, for instance)
|
||||
* @property {string} key unique key to identify the context menu action
|
||||
* (used in custom context menu eg table rows, to identify which actions to include)
|
||||
* @property {boolean} hideInDefaultMenu optional flag to hide action from showing in the default context menu (tree item)
|
||||
*/
|
||||
/**
|
||||
* @method appliesTo
|
||||
* @memberof module:openmct.ContextMenuAction#
|
||||
* @param {DomainObject[]} objectPath the path of the object that the context menu has been invoked on.
|
||||
* @returns {boolean} true if the action applies to the objects specified in the 'objectPath', otherwise false.
|
||||
*/
|
||||
/**
|
||||
* Code to be executed when the action is selected from a context menu
|
||||
* @method invoke
|
||||
* @memberof module:openmct.ContextMenuAction#
|
||||
* @param {DomainObject[]} objectPath the path of the object to invoke the action on.
|
||||
*/
|
||||
/**
|
||||
* @param {ContextMenuAction} actionDefinition
|
||||
*/
|
||||
registerAction(actionDefinition) {
|
||||
this._allActions.push(actionDefinition);
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_showContextMenuForObjectPath(objectPath, x, y, actionsToBeIncluded) {
|
||||
|
||||
let applicableActions = this._allActions.filter((action) => {
|
||||
|
||||
if (actionsToBeIncluded) {
|
||||
if (action.appliesTo === undefined && actionsToBeIncluded.includes(action.key)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return action.appliesTo(objectPath, actionsToBeIncluded) && actionsToBeIncluded.includes(action.key);
|
||||
} else {
|
||||
if (action.appliesTo === undefined) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return action.appliesTo(objectPath) && !action.hideInDefaultMenu;
|
||||
}
|
||||
});
|
||||
|
||||
if (this._activeContextMenu) {
|
||||
this._hideActiveContextMenu();
|
||||
}
|
||||
|
||||
this._activeContextMenu = this._createContextMenuForObject(objectPath, applicableActions);
|
||||
this._activeContextMenu.$mount();
|
||||
document.body.appendChild(this._activeContextMenu.$el);
|
||||
|
||||
let position = this._calculatePopupPosition(x, y, this._activeContextMenu.$el);
|
||||
this._activeContextMenu.$el.style.left = `${position.x}px`;
|
||||
this._activeContextMenu.$el.style.top = `${position.y}px`;
|
||||
|
||||
document.addEventListener('click', this._hideActiveContextMenu);
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_calculatePopupPosition(eventPosX, eventPosY, menuElement) {
|
||||
let menuDimensions = menuElement.getBoundingClientRect();
|
||||
let overflowX = (eventPosX + menuDimensions.width) - document.body.clientWidth;
|
||||
let overflowY = (eventPosY + menuDimensions.height) - document.body.clientHeight;
|
||||
|
||||
if (overflowX > 0) {
|
||||
eventPosX = eventPosX - overflowX;
|
||||
}
|
||||
|
||||
if (overflowY > 0) {
|
||||
eventPosY = eventPosY - overflowY;
|
||||
}
|
||||
|
||||
return {
|
||||
x: eventPosX,
|
||||
y: eventPosY
|
||||
};
|
||||
}
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_hideActiveContextMenu() {
|
||||
document.removeEventListener('click', this._hideActiveContextMenu);
|
||||
document.body.removeChild(this._activeContextMenu.$el);
|
||||
this._activeContextMenu.$destroy();
|
||||
this._activeContextMenu = undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_createContextMenuForObject(objectPath, actions) {
|
||||
return new Vue({
|
||||
components: {
|
||||
ContextMenu: ContextMenuComponent
|
||||
},
|
||||
provide: {
|
||||
actions: actions,
|
||||
objectPath: objectPath
|
||||
},
|
||||
template: '<ContextMenu></ContextMenu>'
|
||||
});
|
||||
}
|
||||
}
|
||||
export default ContextMenuAPI;
|
67
src/api/menu/MenuAPI.js
Normal file
67
src/api/menu/MenuAPI.js
Normal file
@ -0,0 +1,67 @@
|
||||
/*****************************************************************************
|
||||
* 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 Menu from './menu.js';
|
||||
|
||||
/**
|
||||
* The MenuAPI allows the addition of new context menu actions, and for the context menu to be launched from
|
||||
* custom HTML elements.
|
||||
* @interface MenuAPI
|
||||
* @memberof module:openmct
|
||||
*/
|
||||
|
||||
class MenuAPI {
|
||||
constructor(openmct) {
|
||||
this.openmct = openmct;
|
||||
|
||||
this.showMenu = this.showMenu.bind(this);
|
||||
this._clearMenuComponent = this._clearMenuComponent.bind(this);
|
||||
this._showObjectMenu = this._showObjectMenu.bind(this);
|
||||
}
|
||||
|
||||
showMenu(x, y, actions) {
|
||||
if (this.menuComponent) {
|
||||
this.menuComponent.dismiss();
|
||||
}
|
||||
|
||||
let options = {
|
||||
x,
|
||||
y,
|
||||
actions
|
||||
};
|
||||
|
||||
this.menuComponent = new Menu(options);
|
||||
this.menuComponent.once('destroy', this._clearMenuComponent);
|
||||
}
|
||||
|
||||
_clearMenuComponent() {
|
||||
this.menuComponent = undefined;
|
||||
delete this.menuComponent;
|
||||
}
|
||||
|
||||
_showObjectMenu(objectPath, x, y, actionsToBeIncluded) {
|
||||
let applicableActions = this.openmct.actions._groupedAndSortedObjectActions(objectPath, actionsToBeIncluded);
|
||||
|
||||
this.showMenu(x, y, applicableActions);
|
||||
}
|
||||
}
|
||||
export default MenuAPI;
|
125
src/api/menu/MenuAPISpec.js
Normal file
125
src/api/menu/MenuAPISpec.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 MenuAPI from './MenuAPI';
|
||||
import Menu from './menu';
|
||||
import { createOpenMct, resetApplicationState } from '../../utils/testing';
|
||||
|
||||
describe ('The Menu API', () => {
|
||||
let openmct;
|
||||
let menuAPI;
|
||||
let actionsArray;
|
||||
let x;
|
||||
let y;
|
||||
let result;
|
||||
|
||||
beforeEach(() => {
|
||||
openmct = createOpenMct();
|
||||
menuAPI = new MenuAPI(openmct);
|
||||
actionsArray = [
|
||||
{
|
||||
name: 'Test Action 1',
|
||||
cssClass: 'test-css-class-1',
|
||||
description: 'This is a test action',
|
||||
callBack: () => {
|
||||
result = 'Test Action 1 Invoked';
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'Test Action 2',
|
||||
cssClass: 'test-css-class-2',
|
||||
description: 'This is a test action',
|
||||
callBack: () => {
|
||||
result = 'Test Action 2 Invoked';
|
||||
}
|
||||
}
|
||||
];
|
||||
x = 8;
|
||||
y = 16;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
resetApplicationState(openmct);
|
||||
});
|
||||
|
||||
describe("showMenu method", () => {
|
||||
it("creates an instance of Menu when invoked", () => {
|
||||
menuAPI.showMenu(x, y, actionsArray);
|
||||
|
||||
expect(menuAPI.menuComponent).toBeInstanceOf(Menu);
|
||||
});
|
||||
|
||||
describe("creates a menu component", () => {
|
||||
let menuComponent;
|
||||
let vueComponent;
|
||||
|
||||
beforeEach(() => {
|
||||
menuAPI.showMenu(x, y, actionsArray);
|
||||
vueComponent = menuAPI.menuComponent.component;
|
||||
menuComponent = document.querySelector(".c-menu");
|
||||
|
||||
spyOn(vueComponent, '$destroy');
|
||||
});
|
||||
|
||||
it("renders a menu component in the expected x and y coordinates", () => {
|
||||
let boundingClientRect = menuComponent.getBoundingClientRect();
|
||||
let left = boundingClientRect.left;
|
||||
let top = boundingClientRect.top;
|
||||
|
||||
expect(left).toEqual(x);
|
||||
expect(top).toEqual(y);
|
||||
});
|
||||
|
||||
it("with all the actions passed in", () => {
|
||||
expect(menuComponent).toBeDefined();
|
||||
|
||||
let listItems = menuComponent.children[0].children;
|
||||
|
||||
expect(listItems.length).toEqual(actionsArray.length);
|
||||
});
|
||||
|
||||
it("with click-able menu items, that will invoke the correct callBacks", () => {
|
||||
let listItem1 = menuComponent.children[0].children[0];
|
||||
|
||||
listItem1.click();
|
||||
|
||||
expect(result).toEqual("Test Action 1 Invoked");
|
||||
});
|
||||
|
||||
it("dismisses the menu when action is clicked on", () => {
|
||||
let listItem1 = menuComponent.children[0].children[0];
|
||||
|
||||
listItem1.click();
|
||||
|
||||
let menu = document.querySelector('.c-menu');
|
||||
|
||||
expect(menu).toBeNull();
|
||||
});
|
||||
|
||||
it("invokes the destroy method when menu is dismissed", () => {
|
||||
document.body.click();
|
||||
|
||||
expect(vueComponent.$destroy).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
52
src/api/menu/components/Menu.vue
Normal file
52
src/api/menu/components/Menu.vue
Normal file
@ -0,0 +1,52 @@
|
||||
<template>
|
||||
<div class="c-menu">
|
||||
<ul v-if="actions.length && actions[0].length">
|
||||
<template
|
||||
v-for="(actionGroups, index) in actions"
|
||||
>
|
||||
<li
|
||||
v-for="action in actionGroups"
|
||||
:key="action.name"
|
||||
:class="[action.cssClass, action.isDisabled ? 'disabled' : '']"
|
||||
:title="action.description"
|
||||
@click="action.callBack"
|
||||
>
|
||||
{{ action.name }}
|
||||
</li>
|
||||
<div
|
||||
v-if="index !== actions.length - 1"
|
||||
:key="index"
|
||||
class="c-menu__section-separator"
|
||||
>
|
||||
</div>
|
||||
<li
|
||||
v-if="actionGroups.length === 0"
|
||||
:key="index"
|
||||
>
|
||||
No actions defined.
|
||||
</li>
|
||||
</template>
|
||||
</ul>
|
||||
|
||||
<ul v-else>
|
||||
<li
|
||||
v-for="action in actions"
|
||||
:key="action.name"
|
||||
:class="action.cssClass"
|
||||
:title="action.description"
|
||||
@click="action.callBack"
|
||||
>
|
||||
{{ action.name }}
|
||||
</li>
|
||||
<li v-if="actions.length === 0">
|
||||
No actions defined.
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
inject: ['actions']
|
||||
};
|
||||
</script>
|
94
src/api/menu/menu.js
Normal file
94
src/api/menu/menu.js
Normal file
@ -0,0 +1,94 @@
|
||||
/*****************************************************************************
|
||||
* 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 EventEmitter from 'EventEmitter';
|
||||
import MenuComponent from './components/Menu.vue';
|
||||
import Vue from 'vue';
|
||||
|
||||
class Menu extends EventEmitter {
|
||||
constructor(options) {
|
||||
super();
|
||||
|
||||
this.options = options;
|
||||
|
||||
this.component = new Vue({
|
||||
provide: {
|
||||
actions: options.actions
|
||||
},
|
||||
components: {
|
||||
MenuComponent
|
||||
},
|
||||
template: '<menu-component />'
|
||||
});
|
||||
|
||||
if (options.onDestroy) {
|
||||
this.once('destroy', options.onDestroy);
|
||||
}
|
||||
|
||||
this.dismiss = this.dismiss.bind(this);
|
||||
this.show = this.show.bind(this);
|
||||
|
||||
this.show();
|
||||
}
|
||||
|
||||
dismiss() {
|
||||
this.emit('destroy');
|
||||
document.body.removeChild(this.component.$el);
|
||||
document.removeEventListener('click', this.dismiss);
|
||||
this.component.$destroy();
|
||||
}
|
||||
|
||||
show() {
|
||||
this.component.$mount();
|
||||
document.body.appendChild(this.component.$el);
|
||||
|
||||
let position = this._calculatePopupPosition(this.options.x, this.options.y, this.component.$el);
|
||||
|
||||
this.component.$el.style.left = `${position.x}px`;
|
||||
this.component.$el.style.top = `${position.y}px`;
|
||||
|
||||
document.addEventListener('click', this.dismiss);
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_calculatePopupPosition(eventPosX, eventPosY, menuElement) {
|
||||
let menuDimensions = menuElement.getBoundingClientRect();
|
||||
let overflowX = (eventPosX + menuDimensions.width) - document.body.clientWidth;
|
||||
let overflowY = (eventPosY + menuDimensions.height) - document.body.clientHeight;
|
||||
|
||||
if (overflowX > 0) {
|
||||
eventPosX = eventPosX - overflowX;
|
||||
}
|
||||
|
||||
if (overflowY > 0) {
|
||||
eventPosY = eventPosY - overflowY;
|
||||
}
|
||||
|
||||
return {
|
||||
x: eventPosX,
|
||||
y: eventPosY
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export default Menu;
|
@ -47,6 +47,7 @@ define([
|
||||
this.providers = {};
|
||||
this.rootRegistry = new RootRegistry();
|
||||
this.rootProvider = new RootObjectProvider.default(this.rootRegistry);
|
||||
this.cache = {};
|
||||
}
|
||||
|
||||
/**
|
||||
@ -154,6 +155,11 @@ define([
|
||||
* has been saved, or be rejected if it cannot be saved
|
||||
*/
|
||||
ObjectAPI.prototype.get = function (identifier) {
|
||||
let keystring = this.makeKeyString(identifier);
|
||||
if (this.cache[keystring] !== undefined) {
|
||||
return this.cache[keystring];
|
||||
}
|
||||
|
||||
identifier = utils.parseKeyString(identifier);
|
||||
const provider = this.getProvider(identifier);
|
||||
|
||||
@ -165,7 +171,15 @@ define([
|
||||
throw new Error('Provider does not support get!');
|
||||
}
|
||||
|
||||
return provider.get(identifier);
|
||||
let objectPromise = provider.get(identifier);
|
||||
|
||||
this.cache[keystring] = objectPromise;
|
||||
|
||||
return objectPromise.then(result => {
|
||||
delete this.cache[keystring];
|
||||
|
||||
return result;
|
||||
});
|
||||
};
|
||||
|
||||
ObjectAPI.prototype.delete = function () {
|
||||
|
@ -59,4 +59,25 @@ describe("The Object API", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("The get function", () => {
|
||||
describe("when a provider is available", () => {
|
||||
let mockProvider;
|
||||
beforeEach(() => {
|
||||
mockProvider = jasmine.createSpyObj("mock provider", [
|
||||
"get"
|
||||
]);
|
||||
mockProvider.get.and.returnValue(Promise.resolve(mockDomainObject));
|
||||
objectAPI.addProvider(TEST_NAMESPACE, mockProvider);
|
||||
});
|
||||
|
||||
it("Caches multiple requests for the same object", () => {
|
||||
expect(mockProvider.get.calls.count()).toBe(0);
|
||||
objectAPI.get(mockDomainObject.identifier);
|
||||
expect(mockProvider.get.calls.count()).toBe(1);
|
||||
objectAPI.get(mockDomainObject.identifier);
|
||||
expect(mockProvider.get.calls.count()).toBe(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -22,6 +22,7 @@ class OverlayAPI {
|
||||
this.dismissLastOverlay();
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
@ -127,6 +128,7 @@ class OverlayAPI {
|
||||
|
||||
return progressDialog;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default OverlayAPI;
|
||||
|
67
src/api/status/StatusAPI.js
Normal file
67
src/api/status/StatusAPI.js
Normal file
@ -0,0 +1,67 @@
|
||||
/*****************************************************************************
|
||||
* 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 EventEmitter from 'EventEmitter';
|
||||
|
||||
export default class StatusAPI extends EventEmitter {
|
||||
constructor(openmct) {
|
||||
super();
|
||||
|
||||
this._openmct = openmct;
|
||||
this._statusCache = {};
|
||||
|
||||
this.get = this.get.bind(this);
|
||||
this.set = this.set.bind(this);
|
||||
this.observe = this.observe.bind(this);
|
||||
}
|
||||
|
||||
get(identifier) {
|
||||
let keyString = this._openmct.objects.makeKeyString(identifier);
|
||||
|
||||
return this._statusCache[keyString];
|
||||
}
|
||||
|
||||
set(identifier, value) {
|
||||
let keyString = this._openmct.objects.makeKeyString(identifier);
|
||||
|
||||
this._statusCache[keyString] = value;
|
||||
this.emit(keyString, value);
|
||||
}
|
||||
|
||||
delete(identifier) {
|
||||
let keyString = this._openmct.objects.makeKeyString(identifier);
|
||||
|
||||
this._statusCache[keyString] = undefined;
|
||||
this.emit(keyString, undefined);
|
||||
delete this._statusCache[keyString];
|
||||
}
|
||||
|
||||
observe(identifier, callback) {
|
||||
let key = this._openmct.objects.makeKeyString(identifier);
|
||||
|
||||
this.on(key, callback);
|
||||
|
||||
return () => {
|
||||
this.off(key, callback);
|
||||
};
|
||||
}
|
||||
}
|
85
src/api/status/StatusAPISpec.js
Normal file
85
src/api/status/StatusAPISpec.js
Normal file
@ -0,0 +1,85 @@
|
||||
import StatusAPI from './StatusAPI.js';
|
||||
import { createOpenMct, resetApplicationState } from '../../utils/testing';
|
||||
|
||||
describe("The Status API", () => {
|
||||
let statusAPI;
|
||||
let openmct;
|
||||
let identifier;
|
||||
let status;
|
||||
let status2;
|
||||
let callback;
|
||||
|
||||
beforeEach(() => {
|
||||
openmct = createOpenMct();
|
||||
statusAPI = new StatusAPI(openmct);
|
||||
identifier = {
|
||||
namespace: "test-namespace",
|
||||
key: "test-key"
|
||||
};
|
||||
status = "test-status";
|
||||
status2 = 'test-status-deux';
|
||||
callback = jasmine.createSpy('callback', (statusUpdate) => statusUpdate);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
resetApplicationState(openmct);
|
||||
});
|
||||
|
||||
describe("set function", () => {
|
||||
it("sets status for identifier", () => {
|
||||
statusAPI.set(identifier, status);
|
||||
|
||||
let resultingStatus = statusAPI.get(identifier);
|
||||
|
||||
expect(resultingStatus).toEqual(status);
|
||||
});
|
||||
});
|
||||
|
||||
describe("get function", () => {
|
||||
it("returns status for identifier", () => {
|
||||
statusAPI.set(identifier, status2);
|
||||
|
||||
let resultingStatus = statusAPI.get(identifier);
|
||||
|
||||
expect(resultingStatus).toEqual(status2);
|
||||
});
|
||||
});
|
||||
|
||||
describe("delete function", () => {
|
||||
it("deletes status for identifier", () => {
|
||||
statusAPI.set(identifier, status);
|
||||
|
||||
let resultingStatus = statusAPI.get(identifier);
|
||||
expect(resultingStatus).toEqual(status);
|
||||
|
||||
statusAPI.delete(identifier);
|
||||
resultingStatus = statusAPI.get(identifier);
|
||||
|
||||
expect(resultingStatus).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("observe function", () => {
|
||||
|
||||
it("allows callbacks to be attached to status set and delete events", () => {
|
||||
let unsubscribe = statusAPI.observe(identifier, callback);
|
||||
statusAPI.set(identifier, status);
|
||||
|
||||
expect(callback).toHaveBeenCalledWith(status);
|
||||
|
||||
statusAPI.delete(identifier);
|
||||
|
||||
expect(callback).toHaveBeenCalledWith(undefined);
|
||||
unsubscribe();
|
||||
});
|
||||
|
||||
it("returns a unsubscribe function", () => {
|
||||
let unsubscribe = statusAPI.observe(identifier, callback);
|
||||
unsubscribe();
|
||||
|
||||
statusAPI.set(identifier, status);
|
||||
|
||||
expect(callback).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
});
|
||||
});
|
@ -176,7 +176,10 @@ export default {
|
||||
this.timestampKey = timeSystem.key;
|
||||
},
|
||||
showContextMenu(event) {
|
||||
this.openmct.contextMenu._showContextMenuForObjectPath(this.currentObjectPath, event.x, event.y, CONTEXT_MENU_ACTIONS);
|
||||
let allActions = this.openmct.actions.get(this.currentObjectPath, {}, {viewHistoricalData: true});
|
||||
let applicableActions = CONTEXT_MENU_ACTIONS.map(key => allActions[key]);
|
||||
|
||||
this.openmct.menus.showMenu(event.x, event.y, applicableActions);
|
||||
},
|
||||
resetValues() {
|
||||
this.value = '---';
|
||||
|
@ -21,7 +21,7 @@
|
||||
*****************************************************************************/
|
||||
|
||||
<template>
|
||||
<div class="c-lad-table-wrapper">
|
||||
<div class="c-lad-table-wrapper u-style-receiver js-style-receiver">
|
||||
<table class="c-table c-lad-table">
|
||||
<thead>
|
||||
<tr>
|
||||
|
@ -23,6 +23,7 @@
|
||||
export default class ClearDataAction {
|
||||
constructor(openmct, appliesToObjects) {
|
||||
this.name = 'Clear Data for Object';
|
||||
this.key = 'clear-data-action';
|
||||
this.description = 'Clears current data for object, unsubscribes and resubscribes to data';
|
||||
this.cssClass = 'icon-clear-data';
|
||||
|
||||
|
@ -53,7 +53,7 @@ define([
|
||||
openmct.indicators.add(indicator);
|
||||
}
|
||||
|
||||
openmct.contextMenu.registerAction(new ClearDataAction.default(openmct, appliesToObjects));
|
||||
openmct.actions.register(new ClearDataAction.default(openmct, appliesToObjects));
|
||||
};
|
||||
};
|
||||
});
|
||||
|
@ -26,12 +26,12 @@ import ClearDataAction from '../clearDataAction.js';
|
||||
describe('When the Clear Data Plugin is installed,', function () {
|
||||
const mockObjectViews = jasmine.createSpyObj('objectViews', ['emit']);
|
||||
const mockIndicatorProvider = jasmine.createSpyObj('indicators', ['add']);
|
||||
const mockContextMenuProvider = jasmine.createSpyObj('contextMenu', ['registerAction']);
|
||||
const mockActionsProvider = jasmine.createSpyObj('actions', ['register']);
|
||||
|
||||
const openmct = {
|
||||
objectViews: mockObjectViews,
|
||||
indicators: mockIndicatorProvider,
|
||||
contextMenu: mockContextMenuProvider,
|
||||
actions: mockActionsProvider,
|
||||
install: function (plugin) {
|
||||
plugin(this);
|
||||
}
|
||||
@ -51,7 +51,7 @@ describe('When the Clear Data Plugin is installed,', function () {
|
||||
it('Clear Data context menu action is installed', function () {
|
||||
openmct.install(ClearDataActionPlugin([]));
|
||||
|
||||
expect(mockContextMenuProvider.registerAction).toHaveBeenCalled();
|
||||
expect(mockActionsProvider.register).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('clear data action emits a clearData event when invoked', function () {
|
||||
|
@ -50,6 +50,7 @@
|
||||
.c-cs {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1 1 auto;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
|
||||
|
@ -21,21 +21,22 @@
|
||||
*****************************************************************************/
|
||||
|
||||
<template>
|
||||
<div class="c-style">
|
||||
<span :class="[
|
||||
{ 'is-style-invisible': styleItem.style.isStyleInvisible },
|
||||
{ 'c-style-thumb--mixed': mixedStyles.indexOf('backgroundColor') > -1 }
|
||||
]"
|
||||
:style="[styleItem.style.imageUrl ? { backgroundImage:'url(' + styleItem.style.imageUrl + ')'} : itemStyle ]"
|
||||
class="c-style-thumb"
|
||||
>
|
||||
<span class="c-style-thumb__text"
|
||||
:class="{ 'hide-nice': !hasProperty(styleItem.style.color) }"
|
||||
<div class="c-style has-local-controls c-toolbar">
|
||||
<div class="c-style__controls">
|
||||
<div :class="[
|
||||
{ 'is-style-invisible': styleItem.style && styleItem.style.isStyleInvisible },
|
||||
{ 'c-style-thumb--mixed': mixedStyles.indexOf('backgroundColor') > -1 }
|
||||
]"
|
||||
:style="[styleItem.style.imageUrl ? { backgroundImage:'url(' + styleItem.style.imageUrl + ')'} : itemStyle ]"
|
||||
class="c-style-thumb"
|
||||
>
|
||||
ABC
|
||||
</span>
|
||||
</span>
|
||||
<span class="c-toolbar">
|
||||
<span class="c-style-thumb__text"
|
||||
:class="{ 'hide-nice': !hasProperty(styleItem.style.color) }"
|
||||
>
|
||||
ABC
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<toolbar-color-picker v-if="hasProperty(styleItem.style.border)"
|
||||
class="c-style__toolbar-button--border-color u-menu-to--center"
|
||||
:options="borderColorOption"
|
||||
@ -61,7 +62,14 @@
|
||||
:options="isStyleInvisibleOption"
|
||||
@change="updateStyleValue"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Save Styles -->
|
||||
<toolbar-button v-if="canSaveStyle"
|
||||
class="c-style__toolbar-button--save c-local-controls--show-on-hover c-icon-button c-icon-button--major"
|
||||
:options="saveOptions"
|
||||
@click="saveItemStyle()"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -80,12 +88,11 @@ export default {
|
||||
ToolbarColorPicker,
|
||||
ToolbarToggleButton
|
||||
},
|
||||
inject: [
|
||||
'openmct'
|
||||
],
|
||||
inject: ['openmct'],
|
||||
props: {
|
||||
isEditing: {
|
||||
type: Boolean
|
||||
type: Boolean,
|
||||
required: true
|
||||
},
|
||||
mixedStyles: {
|
||||
type: Array,
|
||||
@ -93,6 +100,10 @@ export default {
|
||||
return [];
|
||||
}
|
||||
},
|
||||
nonSpecificFontProperties: {
|
||||
type: Array,
|
||||
required: true
|
||||
},
|
||||
styleItem: {
|
||||
type: Object,
|
||||
required: true
|
||||
@ -182,7 +193,16 @@ export default {
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
},
|
||||
saveOptions() {
|
||||
return {
|
||||
icon: 'icon-save',
|
||||
title: 'Save style',
|
||||
isEditing: this.isEditing
|
||||
};
|
||||
},
|
||||
canSaveStyle() {
|
||||
return this.isEditing && !this.mixedStyles.length && !this.nonSpecificFontProperties.length;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
@ -216,6 +236,9 @@ export default {
|
||||
}
|
||||
|
||||
this.$emit('persist', this.styleItem, item.property);
|
||||
},
|
||||
saveItemStyle() {
|
||||
this.$emit('save-style', this.itemStyle);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -31,6 +31,11 @@
|
||||
<div class="c-inspect-styles__header">
|
||||
Object Style
|
||||
</div>
|
||||
<FontStyleEditor
|
||||
v-if="canStyleFont"
|
||||
:font-style="consolidatedFontStyle"
|
||||
@set-font-property="setFontProperty"
|
||||
/>
|
||||
<div class="c-inspect-styles__content">
|
||||
<div v-if="staticStyle"
|
||||
class="c-inspect-styles__style"
|
||||
@ -39,7 +44,9 @@
|
||||
:style-item="staticStyle"
|
||||
:is-editing="allowEditing"
|
||||
:mixed-styles="mixedStyles"
|
||||
:non-specific-font-properties="nonSpecificFontProperties"
|
||||
@persist="updateStaticStyle"
|
||||
@save-style="saveStyle"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
@ -58,10 +65,11 @@
|
||||
</div>
|
||||
<div class="c-inspect-styles__content c-inspect-styles__condition-set">
|
||||
<a v-if="conditionSetDomainObject"
|
||||
class="c-object-label icon-conditional"
|
||||
class="c-object-label"
|
||||
:href="navigateToPath"
|
||||
@click="navigateOrPreview"
|
||||
>
|
||||
<span class="c-object-label__type-icon icon-conditional"></span>
|
||||
<span class="c-object-label__name">{{ conditionSetDomainObject.name }}</span>
|
||||
</a>
|
||||
<template v-if="allowEditing">
|
||||
@ -80,6 +88,12 @@
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<FontStyleEditor
|
||||
v-if="canStyleFont"
|
||||
:font-style="consolidatedFontStyle"
|
||||
@set-font-property="setFontProperty"
|
||||
/>
|
||||
|
||||
<div v-if="conditionsLoaded"
|
||||
class="c-inspect-styles__conditions"
|
||||
>
|
||||
@ -97,8 +111,10 @@
|
||||
/>
|
||||
<style-editor class="c-inspect-styles__editor"
|
||||
:style-item="conditionStyle"
|
||||
:non-specific-font-properties="nonSpecificFontProperties"
|
||||
:is-editing="allowEditing"
|
||||
@persist="updateConditionalStyle"
|
||||
@save-style="saveStyle"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@ -108,6 +124,7 @@
|
||||
|
||||
<script>
|
||||
|
||||
import FontStyleEditor from '@/ui/inspector/styles/FontStyleEditor.vue';
|
||||
import StyleEditor from "./StyleEditor.vue";
|
||||
import PreviewAction from "@/ui/preview/PreviewAction.js";
|
||||
import { getApplicableStylesForItem, getConsolidatedStyleValues, getConditionSetIdentifierForItem } from "@/plugins/condition/utils/styleUtils";
|
||||
@ -116,16 +133,30 @@ import ConditionError from "@/plugins/condition/components/ConditionError.vue";
|
||||
import ConditionDescription from "@/plugins/condition/components/ConditionDescription.vue";
|
||||
import Vue from 'vue';
|
||||
|
||||
const NON_SPECIFIC = '??';
|
||||
const NON_STYLEABLE_CONTAINER_TYPES = [
|
||||
'layout',
|
||||
'flexible-layout',
|
||||
'tabs'
|
||||
];
|
||||
const NON_STYLEABLE_LAYOUT_ITEM_TYPES = [
|
||||
'line-view',
|
||||
'box-view',
|
||||
'image-view'
|
||||
];
|
||||
|
||||
export default {
|
||||
name: 'StylesView',
|
||||
components: {
|
||||
FontStyleEditor,
|
||||
StyleEditor,
|
||||
ConditionError,
|
||||
ConditionDescription
|
||||
},
|
||||
inject: [
|
||||
'openmct',
|
||||
'selection'
|
||||
'selection',
|
||||
'stylesManager'
|
||||
],
|
||||
data() {
|
||||
return {
|
||||
@ -139,19 +170,80 @@ export default {
|
||||
conditionsLoaded: false,
|
||||
navigateToPath: '',
|
||||
selectedConditionId: '',
|
||||
locked: false
|
||||
items: [],
|
||||
domainObject: undefined,
|
||||
consolidatedFontStyle: {}
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
locked() {
|
||||
return this.selection.some(selectionPath => {
|
||||
const self = selectionPath[0].context.item;
|
||||
const parent = selectionPath.length > 1 ? selectionPath[1].context.item : undefined;
|
||||
|
||||
return (self && self.locked) || (parent && parent.locked);
|
||||
});
|
||||
},
|
||||
allowEditing() {
|
||||
return this.isEditing && !this.locked;
|
||||
},
|
||||
styleableFontItems() {
|
||||
return this.selection.filter(selectionPath => {
|
||||
const item = selectionPath[0].context.item;
|
||||
const itemType = item && item.type;
|
||||
const layoutItem = selectionPath[0].context.layoutItem;
|
||||
const layoutItemType = layoutItem && layoutItem.type;
|
||||
|
||||
if (itemType && NON_STYLEABLE_CONTAINER_TYPES.includes(itemType)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (layoutItemType && NON_STYLEABLE_LAYOUT_ITEM_TYPES.includes(layoutItemType)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
},
|
||||
computedconsolidatedFontStyle() {
|
||||
let consolidatedFontStyle;
|
||||
const styles = [];
|
||||
|
||||
this.styleableFontItems.forEach(styleable => {
|
||||
const fontStyle = this.getFontStyle(styleable[0]);
|
||||
|
||||
styles.push(fontStyle);
|
||||
});
|
||||
|
||||
if (styles.length) {
|
||||
const hasConsolidatedFontSize = styles.length && styles.every((fontStyle, i, arr) => fontStyle.fontSize === arr[0].fontSize);
|
||||
const hasConsolidatedFont = styles.length && styles.every((fontStyle, i, arr) => fontStyle.font === arr[0].font);
|
||||
|
||||
consolidatedFontStyle = {
|
||||
fontSize: hasConsolidatedFontSize ? styles[0].fontSize : NON_SPECIFIC,
|
||||
font: hasConsolidatedFont ? styles[0].font : NON_SPECIFIC
|
||||
};
|
||||
}
|
||||
|
||||
return consolidatedFontStyle;
|
||||
},
|
||||
nonSpecificFontProperties() {
|
||||
if (!this.consolidatedFontStyle) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return Object.keys(this.consolidatedFontStyle).filter(property => this.consolidatedFontStyle[property] === NON_SPECIFIC);
|
||||
},
|
||||
canStyleFont() {
|
||||
return this.styleableFontItems.length && this.allowEditing;
|
||||
}
|
||||
},
|
||||
destroyed() {
|
||||
this.removeListeners();
|
||||
this.openmct.editor.off('isEditing', this.setEditState);
|
||||
this.stylesManager.off('styleSelected', this.applyStyleToSelection);
|
||||
},
|
||||
mounted() {
|
||||
this.items = [];
|
||||
this.previewAction = new PreviewAction(this.openmct);
|
||||
this.isMultipleSelection = this.selection.length > 1;
|
||||
this.getObjectsAndItemsFromSelection();
|
||||
@ -166,7 +258,10 @@ export default {
|
||||
this.initializeStaticStyle();
|
||||
}
|
||||
|
||||
this.setConsolidatedFontStyle();
|
||||
|
||||
this.openmct.editor.on('isEditing', this.setEditState);
|
||||
this.stylesManager.on('styleSelected', this.applyStyleToSelection);
|
||||
},
|
||||
methods: {
|
||||
getObjectStyles() {
|
||||
@ -178,10 +273,10 @@ export default {
|
||||
}
|
||||
} else if (this.items.length) {
|
||||
const itemId = this.items[0].id;
|
||||
if (this.domainObject.configuration && this.domainObject.configuration.objectStyles && this.domainObject.configuration.objectStyles[itemId]) {
|
||||
if (this.domainObject && this.domainObject.configuration && this.domainObject.configuration.objectStyles && this.domainObject.configuration.objectStyles[itemId]) {
|
||||
objectStyles = this.domainObject.configuration.objectStyles[itemId];
|
||||
}
|
||||
} else if (this.domainObject.configuration && this.domainObject.configuration.objectStyles) {
|
||||
} else if (this.domainObject && this.domainObject.configuration && this.domainObject.configuration.objectStyles) {
|
||||
objectStyles = this.domainObject.configuration.objectStyles;
|
||||
}
|
||||
|
||||
@ -219,6 +314,18 @@ export default {
|
||||
isItemType(type, item) {
|
||||
return item && (item.type === type);
|
||||
},
|
||||
canPersistObject(item) {
|
||||
// for now the only way to tell if an object can be persisted is if it is creatable.
|
||||
let creatable = false;
|
||||
if (item) {
|
||||
const type = this.openmct.types.get(item.type);
|
||||
if (type && type.definition) {
|
||||
creatable = (type.definition.creatable === true);
|
||||
}
|
||||
}
|
||||
|
||||
return creatable;
|
||||
},
|
||||
hasConditionalStyle(domainObject, layoutItem) {
|
||||
const id = layoutItem ? layoutItem.id : undefined;
|
||||
|
||||
@ -235,13 +342,8 @@ export default {
|
||||
this.selection.forEach((selectionItem) => {
|
||||
const item = selectionItem[0].context.item;
|
||||
const layoutItem = selectionItem[0].context.layoutItem;
|
||||
const layoutDomainObject = selectionItem[0].context.item;
|
||||
const isChildItem = selectionItem.length > 1;
|
||||
|
||||
if (layoutDomainObject && layoutDomainObject.locked) {
|
||||
this.locked = true;
|
||||
}
|
||||
|
||||
if (!isChildItem) {
|
||||
domainObject = item;
|
||||
itemStyle = getApplicableStylesForItem(item);
|
||||
@ -251,7 +353,7 @@ export default {
|
||||
} else {
|
||||
this.canHide = true;
|
||||
domainObject = selectionItem[1].context.item;
|
||||
if (item && !layoutItem || this.isItemType('subobject-view', layoutItem)) {
|
||||
if (item && !layoutItem || (this.isItemType('subobject-view', layoutItem) && this.canPersistObject(item))) {
|
||||
subObjects.push(item);
|
||||
itemStyle = getApplicableStylesForItem(item);
|
||||
if (this.hasConditionalStyle(item)) {
|
||||
@ -275,7 +377,7 @@ export default {
|
||||
const {styles, mixedStyles} = getConsolidatedStyleValues(itemInitialStyles);
|
||||
this.initialStyles = styles;
|
||||
this.mixedStyles = mixedStyles;
|
||||
|
||||
// main layout
|
||||
this.domainObject = domainObject;
|
||||
this.removeListeners();
|
||||
if (this.domainObject) {
|
||||
@ -298,6 +400,7 @@ export default {
|
||||
isKeyItemId(key) {
|
||||
return (key !== 'styles')
|
||||
&& (key !== 'staticStyle')
|
||||
&& (key !== 'fontStyle')
|
||||
&& (key !== 'defaultConditionId')
|
||||
&& (key !== 'selectedConditionId')
|
||||
&& (key !== 'conditionSetIdentifier');
|
||||
@ -637,6 +740,124 @@ export default {
|
||||
},
|
||||
persist(domainObject, style) {
|
||||
this.openmct.objects.mutate(domainObject, 'configuration.objectStyles', style);
|
||||
},
|
||||
applyStyleToSelection(style) {
|
||||
if (!this.allowEditing) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.updateSelectionFontStyle(style);
|
||||
this.updateSelectionStyle(style);
|
||||
},
|
||||
updateSelectionFontStyle(style) {
|
||||
const fontSizeProperty = {
|
||||
fontSize: style.fontSize
|
||||
};
|
||||
const fontProperty = {
|
||||
font: style.font
|
||||
};
|
||||
|
||||
this.setFontProperty(fontSizeProperty);
|
||||
this.setFontProperty(fontProperty);
|
||||
},
|
||||
updateSelectionStyle(style) {
|
||||
const foundStyle = this.findStyleByConditionId(this.selectedConditionId);
|
||||
|
||||
if (foundStyle && !this.isStaticAndConditionalStyles) {
|
||||
Object.entries(style).forEach(([property, value]) => {
|
||||
if (foundStyle.style[property] !== undefined && foundStyle.style[property] !== value) {
|
||||
foundStyle.style[property] = value;
|
||||
}
|
||||
});
|
||||
this.getAndPersistStyles();
|
||||
} else {
|
||||
this.removeConditionSet();
|
||||
Object.entries(style).forEach(([property, value]) => {
|
||||
if (this.staticStyle.style[property] !== undefined && this.staticStyle.style[property] !== value) {
|
||||
this.staticStyle.style[property] = value;
|
||||
this.getAndPersistStyles(property);
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
saveStyle(style) {
|
||||
const styleToSave = {
|
||||
...style,
|
||||
...this.consolidatedFontStyle
|
||||
};
|
||||
|
||||
this.stylesManager.save(styleToSave);
|
||||
},
|
||||
setConsolidatedFontStyle() {
|
||||
const styles = [];
|
||||
|
||||
this.styleableFontItems.forEach(styleable => {
|
||||
const fontStyle = this.getFontStyle(styleable[0]);
|
||||
|
||||
styles.push(fontStyle);
|
||||
});
|
||||
|
||||
if (styles.length) {
|
||||
const hasConsolidatedFontSize = styles.length && styles.every((fontStyle, i, arr) => fontStyle.fontSize === arr[0].fontSize);
|
||||
const hasConsolidatedFont = styles.length && styles.every((fontStyle, i, arr) => fontStyle.font === arr[0].font);
|
||||
|
||||
const fontSize = hasConsolidatedFontSize ? styles[0].fontSize : NON_SPECIFIC;
|
||||
const font = hasConsolidatedFont ? styles[0].font : NON_SPECIFIC;
|
||||
|
||||
this.$set(this.consolidatedFontStyle, 'fontSize', fontSize);
|
||||
this.$set(this.consolidatedFontStyle, 'font', font);
|
||||
}
|
||||
},
|
||||
getFontStyle(selectionPath) {
|
||||
const item = selectionPath.context.item;
|
||||
const layoutItem = selectionPath.context.layoutItem;
|
||||
let fontStyle = item && item.configuration && item.configuration.fontStyle;
|
||||
|
||||
// support for legacy where font styling in layouts only
|
||||
if (!fontStyle) {
|
||||
fontStyle = {
|
||||
fontSize: layoutItem && layoutItem.fontSize || 'default',
|
||||
font: layoutItem && layoutItem.font || 'default'
|
||||
};
|
||||
}
|
||||
|
||||
return fontStyle;
|
||||
},
|
||||
setFontProperty(fontStyleObject) {
|
||||
let layoutDomainObject;
|
||||
const [property, value] = Object.entries(fontStyleObject)[0];
|
||||
|
||||
this.styleableFontItems.forEach(styleable => {
|
||||
if (!this.isLayoutObject(styleable)) {
|
||||
const fontStyle = this.getFontStyle(styleable[0]);
|
||||
fontStyle[property] = value;
|
||||
|
||||
this.openmct.objects.mutate(styleable[0].context.item, 'configuration.fontStyle', fontStyle);
|
||||
} else {
|
||||
// all layoutItems in this context will share same parent layout
|
||||
if (!layoutDomainObject) {
|
||||
layoutDomainObject = styleable[1].context.item;
|
||||
}
|
||||
|
||||
// save layout item font style to parent layout configuration
|
||||
const layoutItemIndex = styleable[0].context.index;
|
||||
const layoutItemConfiguration = layoutDomainObject.configuration.items[layoutItemIndex];
|
||||
|
||||
layoutItemConfiguration[property] = value;
|
||||
}
|
||||
});
|
||||
|
||||
if (layoutDomainObject) {
|
||||
this.openmct.objects.mutate(layoutDomainObject, 'configuration.items', layoutDomainObject.configuration.items);
|
||||
}
|
||||
|
||||
// sync vue component on font update
|
||||
this.$set(this.consolidatedFontStyle, property, value);
|
||||
},
|
||||
isLayoutObject(selectionPath) {
|
||||
const layoutItemType = selectionPath[0].context.layoutItem && selectionPath[0].context.layoutItem.type;
|
||||
|
||||
return layoutItemType && layoutItemType !== 'subobject-view';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -40,9 +40,11 @@
|
||||
}
|
||||
|
||||
&__condition-set {
|
||||
align-items: baseline;
|
||||
border-bottom: 1px solid $colorInteriorBorder;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding-bottom: $interiorMargin;
|
||||
|
||||
.c-object-label {
|
||||
flex: 1 1 auto;
|
||||
@ -53,7 +55,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
&__style,
|
||||
&__style {
|
||||
padding-bottom: $interiorMargin;
|
||||
}
|
||||
|
||||
&__condition {
|
||||
padding: $interiorMargin;
|
||||
}
|
||||
|
@ -146,6 +146,8 @@ describe('the plugin', function () {
|
||||
let displayLayoutItem;
|
||||
let lineLayoutItem;
|
||||
let boxLayoutItem;
|
||||
let notCreatableObjectItem;
|
||||
let notCreatableObject;
|
||||
let selection;
|
||||
let component;
|
||||
let styleViewComponentObject;
|
||||
@ -264,6 +266,19 @@ describe('the plugin', function () {
|
||||
"stroke": "#717171",
|
||||
"type": "line-view",
|
||||
"id": "57d49a28-7863-43bd-9593-6570758916f0"
|
||||
},
|
||||
{
|
||||
"width": 32,
|
||||
"height": 18,
|
||||
"x": 36,
|
||||
"y": 8,
|
||||
"identifier": {
|
||||
"key": "~TEST~image",
|
||||
"namespace": "test-space"
|
||||
},
|
||||
"hasFrame": true,
|
||||
"type": "subobject-view",
|
||||
"id": "6d9fe81b-a3ce-4e59-b404-a4a0be1a5d85"
|
||||
}
|
||||
],
|
||||
"layoutGrid": [
|
||||
@ -297,6 +312,52 @@ describe('the plugin', function () {
|
||||
"type": "box-view",
|
||||
"id": "89b88746-d325-487b-aec4-11b79afff9e8"
|
||||
};
|
||||
notCreatableObjectItem = {
|
||||
"width": 32,
|
||||
"height": 18,
|
||||
"x": 36,
|
||||
"y": 8,
|
||||
"identifier": {
|
||||
"key": "~TEST~image",
|
||||
"namespace": "test-space"
|
||||
},
|
||||
"hasFrame": true,
|
||||
"type": "subobject-view",
|
||||
"id": "6d9fe81b-a3ce-4e59-b404-a4a0be1a5d85"
|
||||
};
|
||||
notCreatableObject = {
|
||||
"identifier": {
|
||||
"key": "~TEST~image",
|
||||
"namespace": "test-space"
|
||||
},
|
||||
"name": "test~image",
|
||||
"location": "test-space:~TEST",
|
||||
"type": "test.image",
|
||||
"telemetry": {
|
||||
"values": [
|
||||
{
|
||||
"key": "value",
|
||||
"name": "Value",
|
||||
"hints": {
|
||||
"image": 1,
|
||||
"priority": 0
|
||||
},
|
||||
"format": "image",
|
||||
"source": "value"
|
||||
},
|
||||
{
|
||||
"key": "utc",
|
||||
"source": "timestamp",
|
||||
"name": "Timestamp",
|
||||
"format": "iso",
|
||||
"hints": {
|
||||
"domain": 1,
|
||||
"priority": 1
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
selection = [
|
||||
[{
|
||||
context: {
|
||||
@ -316,6 +377,19 @@ describe('the plugin', function () {
|
||||
"index": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
context: {
|
||||
item: displayLayoutItem,
|
||||
"supportsMultiSelect": true
|
||||
}
|
||||
}],
|
||||
[{
|
||||
context: {
|
||||
"item": notCreatableObject,
|
||||
"layoutItem": notCreatableObjectItem,
|
||||
"index": 2
|
||||
}
|
||||
},
|
||||
{
|
||||
context: {
|
||||
item: displayLayoutItem,
|
||||
@ -344,7 +418,7 @@ describe('the plugin', function () {
|
||||
});
|
||||
|
||||
it('initializes the items in the view', () => {
|
||||
expect(styleViewComponentObject.items.length).toBe(2);
|
||||
expect(styleViewComponentObject.items.length).toBe(3);
|
||||
});
|
||||
|
||||
it('initializes conditional styles', () => {
|
||||
@ -363,7 +437,7 @@ describe('the plugin', function () {
|
||||
|
||||
return Vue.nextTick().then(() => {
|
||||
expect(styleViewComponentObject.domainObject.configuration.objectStyles).toBeDefined();
|
||||
[boxLayoutItem, lineLayoutItem].forEach((item) => {
|
||||
[boxLayoutItem, lineLayoutItem, notCreatableObjectItem].forEach((item) => {
|
||||
const itemStyles = styleViewComponentObject.domainObject.configuration.objectStyles[item.id].styles;
|
||||
expect(itemStyles.length).toBe(2);
|
||||
const foundStyle = itemStyles.find((style) => {
|
||||
@ -385,7 +459,7 @@ describe('the plugin', function () {
|
||||
|
||||
return Vue.nextTick().then(() => {
|
||||
expect(styleViewComponentObject.domainObject.configuration.objectStyles).toBeDefined();
|
||||
[boxLayoutItem, lineLayoutItem].forEach((item) => {
|
||||
[boxLayoutItem, lineLayoutItem, notCreatableObjectItem].forEach((item) => {
|
||||
const itemStyle = styleViewComponentObject.domainObject.configuration.objectStyles[item.id].staticStyle;
|
||||
expect(itemStyle).toBeDefined();
|
||||
const applicableStyles = getApplicableStylesForItem(styleViewComponentObject.domainObject, item);
|
||||
|
@ -22,7 +22,7 @@
|
||||
|
||||
<template>
|
||||
<component :is="urlDefined ? 'a' : 'span'"
|
||||
class="c-condition-widget"
|
||||
class="c-condition-widget u-style-receiver js-style-receiver"
|
||||
:href="urlDefined ? internalDomainObject.url : null"
|
||||
>
|
||||
<div class="c-condition-widget__label">
|
||||
|
@ -64,9 +64,16 @@ define([
|
||||
components: {
|
||||
AlphanumericFormatView: AlphanumericFormatView.default
|
||||
},
|
||||
template: '<alphanumeric-format-view></alphanumeric-format-view>'
|
||||
template: '<alphanumeric-format-view ref="alphanumericFormatView"></alphanumeric-format-view>'
|
||||
});
|
||||
},
|
||||
getViewContext() {
|
||||
if (component) {
|
||||
return component.$refs.alphanumericFormatView.getViewContext();
|
||||
} else {
|
||||
return {};
|
||||
}
|
||||
},
|
||||
destroy: function () {
|
||||
component.$destroy();
|
||||
component = undefined;
|
||||
|
@ -73,7 +73,6 @@ define(['lodash'], function (_) {
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
const VIEW_TYPES = {
|
||||
'telemetry-view': {
|
||||
value: 'telemetry-view',
|
||||
@ -96,7 +95,6 @@ define(['lodash'], function (_) {
|
||||
class: 'icon-tabular-realtime'
|
||||
}
|
||||
};
|
||||
|
||||
const APPLICABLE_VIEWS = {
|
||||
'telemetry-view': [
|
||||
VIEW_TYPES['telemetry.plot.overlay'],
|
||||
@ -390,29 +388,6 @@ define(['lodash'], function (_) {
|
||||
}
|
||||
}
|
||||
|
||||
function getTextSizeMenu(selectedParent, selection) {
|
||||
const TEXT_SIZE = [8, 9, 10, 11, 12, 13, 14, 15, 16, 20, 24, 30, 36, 48, 72, 96, 128];
|
||||
|
||||
return {
|
||||
control: "select-menu",
|
||||
domainObject: selectedParent,
|
||||
applicableSelectedItems: selection.filter(selectionPath => {
|
||||
let type = selectionPath[0].context.layoutItem.type;
|
||||
|
||||
return type === 'text-view' || type === 'telemetry-view';
|
||||
}),
|
||||
property: function (selectionPath) {
|
||||
return getPath(selectionPath) + ".size";
|
||||
},
|
||||
title: "Set text size",
|
||||
options: TEXT_SIZE.map(size => {
|
||||
return {
|
||||
value: size + "px"
|
||||
};
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
function getTextButton(selectedParent, selection) {
|
||||
return {
|
||||
control: "button",
|
||||
@ -423,7 +398,7 @@ define(['lodash'], function (_) {
|
||||
property: function (selectionPath) {
|
||||
return getPath(selectionPath);
|
||||
},
|
||||
icon: "icon-font",
|
||||
icon: "icon-pencil",
|
||||
title: "Edit text properties",
|
||||
dialog: DIALOG_FORM.text
|
||||
};
|
||||
@ -623,6 +598,33 @@ define(['lodash'], function (_) {
|
||||
}
|
||||
}
|
||||
|
||||
function getToggleGridButton(selection, selectionPath) {
|
||||
const ICON_GRID_SHOW = 'icon-grid-on';
|
||||
const ICON_GRID_HIDE = 'icon-grid-off';
|
||||
|
||||
let displayLayoutContext;
|
||||
|
||||
if (selection.length === 1 && selectionPath === undefined) {
|
||||
displayLayoutContext = selection[0][0].context;
|
||||
} else {
|
||||
displayLayoutContext = selectionPath[1].context;
|
||||
}
|
||||
|
||||
return {
|
||||
control: "button",
|
||||
domainObject: displayLayoutContext.item,
|
||||
icon: ICON_GRID_SHOW,
|
||||
method: function () {
|
||||
displayLayoutContext.toggleGrid();
|
||||
|
||||
this.icon = this.icon === ICON_GRID_SHOW
|
||||
? ICON_GRID_HIDE
|
||||
: ICON_GRID_SHOW;
|
||||
},
|
||||
secondary: true
|
||||
};
|
||||
}
|
||||
|
||||
function getSeparator() {
|
||||
return {
|
||||
control: "separator"
|
||||
@ -637,7 +639,9 @@ define(['lodash'], function (_) {
|
||||
}
|
||||
|
||||
if (isMainLayoutSelected(selectedObjects[0])) {
|
||||
return [getAddButton(selectedObjects)];
|
||||
return [
|
||||
getToggleGridButton(selectedObjects),
|
||||
getAddButton(selectedObjects)];
|
||||
}
|
||||
|
||||
let toolbar = {
|
||||
@ -649,11 +653,11 @@ define(['lodash'], function (_) {
|
||||
'display-mode': [],
|
||||
'telemetry-value': [],
|
||||
'style': [],
|
||||
'text-style': [],
|
||||
'position': [],
|
||||
'duplicate': [],
|
||||
'unit-toggle': [],
|
||||
'remove': []
|
||||
'remove': [],
|
||||
'toggle-grid': []
|
||||
};
|
||||
|
||||
selectedObjects.forEach(selectionPath => {
|
||||
@ -699,12 +703,6 @@ define(['lodash'], function (_) {
|
||||
toolbar['telemetry-value'] = [getTelemetryValueMenu(selectionPath, selectedObjects)];
|
||||
}
|
||||
|
||||
if (toolbar['text-style'].length === 0) {
|
||||
toolbar['text-style'] = [
|
||||
getTextSizeMenu(selectedParent, selectedObjects)
|
||||
];
|
||||
}
|
||||
|
||||
if (toolbar.position.length === 0) {
|
||||
toolbar.position = [
|
||||
getStackOrder(selectedParent, selectionPath),
|
||||
@ -730,12 +728,6 @@ define(['lodash'], function (_) {
|
||||
}
|
||||
}
|
||||
} else if (layoutItem.type === 'text-view') {
|
||||
if (toolbar['text-style'].length === 0) {
|
||||
toolbar['text-style'] = [
|
||||
getTextSizeMenu(selectedParent, selectedObjects)
|
||||
];
|
||||
}
|
||||
|
||||
if (toolbar.position.length === 0) {
|
||||
toolbar.position = [
|
||||
getStackOrder(selectedParent, selectionPath),
|
||||
@ -800,6 +792,10 @@ define(['lodash'], function (_) {
|
||||
if (toolbar.duplicate.length === 0) {
|
||||
toolbar.duplicate = [getDuplicateButton(selectedParent, selectionPath, selectedObjects)];
|
||||
}
|
||||
|
||||
if (toolbar['toggle-grid'].length === 0) {
|
||||
toolbar['toggle-grid'] = [getToggleGridButton(selectedObjects, selectionPath)];
|
||||
}
|
||||
});
|
||||
|
||||
let toolbarArray = Object.values(toolbar);
|
||||
|
@ -56,6 +56,28 @@ define(function () {
|
||||
1
|
||||
],
|
||||
required: true
|
||||
},
|
||||
{
|
||||
name: "Horizontal size (px)",
|
||||
control: "numberfield",
|
||||
cssClass: "l-input-sm l-numeric",
|
||||
property: [
|
||||
"configuration",
|
||||
"layoutDimensions",
|
||||
0
|
||||
],
|
||||
required: false
|
||||
},
|
||||
{
|
||||
name: "Vertical size (px)",
|
||||
control: "numberfield",
|
||||
cssClass: "l-input-sm l-numeric",
|
||||
property: [
|
||||
"configuration",
|
||||
"layoutDimensions",
|
||||
1
|
||||
],
|
||||
required: false
|
||||
}
|
||||
]
|
||||
};
|
||||
|
33
src/plugins/displayLayout/actions/CopyToClipboardAction.js
Normal file
33
src/plugins/displayLayout/actions/CopyToClipboardAction.js
Normal file
@ -0,0 +1,33 @@
|
||||
import clipboard from '@/utils/clipboard';
|
||||
|
||||
export default class CopyToClipboardAction {
|
||||
constructor(openmct) {
|
||||
this.openmct = openmct;
|
||||
|
||||
this.cssClass = 'icon-duplicate';
|
||||
this.description = 'Copy to Clipboard action';
|
||||
this.group = "action";
|
||||
this.key = 'copyToClipboard';
|
||||
this.name = 'Copy to Clipboard';
|
||||
this.priority = 9;
|
||||
}
|
||||
|
||||
invoke(objectPath, viewContext) {
|
||||
const formattedValue = viewContext.formattedValueForCopy();
|
||||
clipboard.updateClipboard(formattedValue)
|
||||
.then(() => {
|
||||
this.openmct.notifications.info(`Success : copied to clipboard '${formattedValue}'`);
|
||||
})
|
||||
.catch(() => {
|
||||
this.openmct.notifications.error(`Failed : to copy to clipboard '${formattedValue}'`);
|
||||
});
|
||||
}
|
||||
|
||||
appliesTo(objectPath, viewContext) {
|
||||
if (viewContext && viewContext.getViewKey) {
|
||||
return viewContext.getViewKey().includes('alphanumeric-format');
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
@ -29,7 +29,7 @@
|
||||
@endMove="() => $emit('endMove')"
|
||||
>
|
||||
<div
|
||||
class="c-box-view"
|
||||
class="c-box-view u-style-receiver js-style-receiver"
|
||||
:class="[styleClass]"
|
||||
:style="style"
|
||||
></div>
|
||||
|
@ -22,7 +22,7 @@
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="l-layout"
|
||||
class="l-layout u-style-receiver js-style-receiver"
|
||||
:class="{
|
||||
'is-multi-selected': selectedLayoutItems.length > 1,
|
||||
'allow-editing': isEditing
|
||||
@ -31,21 +31,19 @@
|
||||
@click.capture="bypassSelection"
|
||||
@drop="handleDrop"
|
||||
>
|
||||
<!-- Background grid -->
|
||||
<div
|
||||
<display-layout-grid
|
||||
v-if="isEditing"
|
||||
class="l-layout__grid-holder c-grid"
|
||||
:grid-size="gridSize"
|
||||
:show-grid="showGrid"
|
||||
/>
|
||||
<div
|
||||
v-if="shouldDisplayLayoutDimensions"
|
||||
class="l-layout__dimensions"
|
||||
:style="layoutDimensionsStyle"
|
||||
>
|
||||
<div
|
||||
v-if="gridSize[0] >= 3"
|
||||
class="c-grid__x l-grid l-grid-x"
|
||||
:style="[{ backgroundSize: gridSize[0] + 'px 100%' }]"
|
||||
></div>
|
||||
<div
|
||||
v-if="gridSize[1] >= 3"
|
||||
class="c-grid__y l-grid l-grid-y"
|
||||
:style="[{ backgroundSize: '100%' + gridSize[1] + 'px' }]"
|
||||
></div>
|
||||
<div class="l-layout__dimensions-vals">
|
||||
{{ layoutDimensions[0] }},{{ layoutDimensions[1] }}
|
||||
</div>
|
||||
</div>
|
||||
<component
|
||||
:is="item.type"
|
||||
@ -81,6 +79,7 @@ import TextView from './TextView.vue';
|
||||
import LineView from './LineView.vue';
|
||||
import ImageView from './ImageView.vue';
|
||||
import EditMarquee from './EditMarquee.vue';
|
||||
import DisplayLayoutGrid from './DisplayLayoutGrid.vue';
|
||||
import _ from 'lodash';
|
||||
|
||||
const TELEMETRY_IDENTIFIER_FUNCTIONS = {
|
||||
@ -127,6 +126,7 @@ const DUPLICATE_OFFSET = 3;
|
||||
|
||||
let components = ITEM_TYPE_VIEW_MAP;
|
||||
components['edit-marquee'] = EditMarquee;
|
||||
components['display-layout-grid'] = DisplayLayoutGrid;
|
||||
|
||||
function getItemDefinition(itemType, ...options) {
|
||||
let itemView = ITEM_TYPE_VIEW_MAP[itemType];
|
||||
@ -140,6 +140,7 @@ function getItemDefinition(itemType, ...options) {
|
||||
|
||||
export default {
|
||||
components: components,
|
||||
inject: ['openmct', 'options', 'objectPath'],
|
||||
props: {
|
||||
domainObject: {
|
||||
type: Object,
|
||||
@ -156,7 +157,8 @@ export default {
|
||||
return {
|
||||
internalDomainObject: domainObject,
|
||||
initSelectIndex: undefined,
|
||||
selection: []
|
||||
selection: [],
|
||||
showGrid: true
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@ -171,6 +173,23 @@ export default {
|
||||
return this.itemIsInCurrentSelection(item);
|
||||
});
|
||||
},
|
||||
layoutDimensions() {
|
||||
return this.internalDomainObject.configuration.layoutDimensions;
|
||||
},
|
||||
shouldDisplayLayoutDimensions() {
|
||||
return this.layoutDimensions
|
||||
&& this.layoutDimensions[0] > 0
|
||||
&& this.layoutDimensions[1] > 0;
|
||||
},
|
||||
layoutDimensionsStyle() {
|
||||
const width = `${this.layoutDimensions[0]}px`;
|
||||
const height = `${this.layoutDimensions[1]}px`;
|
||||
|
||||
return {
|
||||
width,
|
||||
height
|
||||
};
|
||||
},
|
||||
showMarquee() {
|
||||
let selectionPath = this.selection[0];
|
||||
let singleSelectedLine = this.selection.length === 1
|
||||
@ -179,7 +198,13 @@ export default {
|
||||
return this.isEditing && selectionPath && selectionPath.length > 1 && !singleSelectedLine;
|
||||
}
|
||||
},
|
||||
inject: ['openmct', 'options', 'objectPath'],
|
||||
watch: {
|
||||
isEditing(value) {
|
||||
if (value) {
|
||||
this.showGrid = value;
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.unlisten = this.openmct.objects.observe(this.internalDomainObject, '*', function (obj) {
|
||||
this.internalDomainObject = JSON.parse(JSON.stringify(obj));
|
||||
@ -798,6 +823,9 @@ export default {
|
||||
|
||||
this.removeItem(selection);
|
||||
this.initSelectIndex = this.layoutItems.length - 1; //restore selection
|
||||
},
|
||||
toggleGrid() {
|
||||
this.showGrid = !this.showGrid;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
34
src/plugins/displayLayout/components/DisplayLayoutGrid.vue
Normal file
34
src/plugins/displayLayout/components/DisplayLayoutGrid.vue
Normal file
@ -0,0 +1,34 @@
|
||||
<template>
|
||||
<div
|
||||
class="l-layout__grid-holder"
|
||||
:class="{ 'c-grid': showGrid }"
|
||||
>
|
||||
<div
|
||||
v-if="gridSize[0] >= 3"
|
||||
class="c-grid__x l-grid l-grid-x"
|
||||
:style="[{ backgroundSize: gridSize[0] + 'px 100%' }]"
|
||||
></div>
|
||||
<div
|
||||
v-if="gridSize[1] >= 3"
|
||||
class="c-grid__y l-grid l-grid-y"
|
||||
:style="[{ backgroundSize: '100%' + gridSize[1] + 'px' }]"
|
||||
></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
gridSize: {
|
||||
type: Array,
|
||||
required: true,
|
||||
validator: (arr) => arr && arr.length === 2
|
||||
&& arr.every(el => typeof el === 'number')
|
||||
},
|
||||
showGrid: {
|
||||
type: Boolean,
|
||||
required: true
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
@ -81,6 +81,7 @@ export default {
|
||||
style() {
|
||||
let backgroundImage = 'url(' + this.item.url + ')';
|
||||
let border = '1px solid ' + this.item.stroke;
|
||||
|
||||
if (this.itemStyle) {
|
||||
if (this.itemStyle.imageUrl !== undefined) {
|
||||
backgroundImage = 'url(' + this.itemStyle.imageUrl + ')';
|
||||
|
@ -35,6 +35,8 @@
|
||||
:object-path="currentObjectPath"
|
||||
:has-frame="item.hasFrame"
|
||||
:show-edit-view="false"
|
||||
:layout-font-size="item.fontSize"
|
||||
:layout-font="item.font"
|
||||
/>
|
||||
</layout-frame>
|
||||
</template>
|
||||
@ -73,6 +75,8 @@ export default {
|
||||
y: position[1],
|
||||
identifier: domainObject.identifier,
|
||||
hasFrame: hasFrameByDefault(domainObject.type),
|
||||
fontSize: 'default',
|
||||
font: 'default',
|
||||
viewKey
|
||||
};
|
||||
},
|
||||
@ -138,14 +142,18 @@ export default {
|
||||
this.domainObject = domainObject;
|
||||
this.currentObjectPath = [this.domainObject].concat(this.objectPath.slice());
|
||||
this.$nextTick(() => {
|
||||
let childContext = this.$refs.objectFrame.getSelectionContext();
|
||||
childContext.item = domainObject;
|
||||
childContext.layoutItem = this.item;
|
||||
childContext.index = this.index;
|
||||
this.context = childContext;
|
||||
this.removeSelectable = this.openmct.selection.selectable(
|
||||
this.$el, this.context, this.immediatelySelect || this.initSelect);
|
||||
delete this.immediatelySelect;
|
||||
let reference = this.$refs.objectFrame;
|
||||
|
||||
if (reference) {
|
||||
let childContext = this.$refs.objectFrame.getSelectionContext();
|
||||
childContext.item = domainObject;
|
||||
childContext.layoutItem = this.item;
|
||||
childContext.index = this.index;
|
||||
this.context = childContext;
|
||||
this.removeSelectable = this.openmct.selection.selectable(
|
||||
this.$el, this.context, this.immediatelySelect || this.initSelect);
|
||||
delete this.immediatelySelect;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -31,15 +31,14 @@
|
||||
<div
|
||||
v-if="domainObject"
|
||||
class="c-telemetry-view"
|
||||
:class="{
|
||||
styleClass,
|
||||
'is-missing': domainObject.status === 'missing'
|
||||
}"
|
||||
:class="[statusClass]"
|
||||
:style="styleObject"
|
||||
:data-font-size="item.fontSize"
|
||||
:data-font="item.font"
|
||||
@contextmenu.prevent="showContextMenu"
|
||||
>
|
||||
<div class="is-missing__indicator"
|
||||
title="This item is missing"
|
||||
<div class="is-status__indicator"
|
||||
title="This item is missing or suspect"
|
||||
></div>
|
||||
<div
|
||||
v-if="showLabel"
|
||||
@ -74,10 +73,11 @@
|
||||
import LayoutFrame from './LayoutFrame.vue';
|
||||
import printj from 'printj';
|
||||
import conditionalStylesMixin from "../mixins/objectStyles-mixin";
|
||||
import { getDefaultNotebook } from '@/plugins/notebook/utils/notebook-storage.js';
|
||||
|
||||
const DEFAULT_TELEMETRY_DIMENSIONS = [10, 5];
|
||||
const DEFAULT_POSITION = [1, 1];
|
||||
const CONTEXT_MENU_ACTIONS = ['viewHistoricalData'];
|
||||
const CONTEXT_MENU_ACTIONS = ['copyToClipboard', 'copyToNotebook', 'viewHistoricalData'];
|
||||
|
||||
export default {
|
||||
makeDefinition(openmct, gridSize, domainObject, position) {
|
||||
@ -95,7 +95,8 @@ export default {
|
||||
stroke: "",
|
||||
fill: "",
|
||||
color: "",
|
||||
size: "13px"
|
||||
fontSize: 'default',
|
||||
font: 'default'
|
||||
};
|
||||
},
|
||||
inject: ['openmct', 'objectPath'],
|
||||
@ -126,13 +127,18 @@ export default {
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
currentObjectPath: undefined,
|
||||
datum: undefined,
|
||||
formats: undefined,
|
||||
domainObject: undefined,
|
||||
currentObjectPath: undefined
|
||||
formats: undefined,
|
||||
viewKey: `alphanumeric-format-${Math.random()}`,
|
||||
status: ''
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
statusClass() {
|
||||
return (this.status) ? `is-status--${this.status}` : '';
|
||||
},
|
||||
showLabel() {
|
||||
let displayMode = this.item.displayMode;
|
||||
|
||||
@ -150,10 +156,15 @@ export default {
|
||||
return unit;
|
||||
},
|
||||
styleObject() {
|
||||
return Object.assign({}, {
|
||||
fontSize: this.item.size
|
||||
}, this.itemStyle);
|
||||
let size;
|
||||
//for legacy size support
|
||||
if (!this.item.fontSize) {
|
||||
size = this.item.size;
|
||||
}
|
||||
|
||||
return Object.assign({}, {
|
||||
size
|
||||
}, this.itemStyle);
|
||||
},
|
||||
fieldName() {
|
||||
return this.valueMetadata && this.valueMetadata.name;
|
||||
@ -205,9 +216,13 @@ export default {
|
||||
this.openmct.objects.get(this.item.identifier)
|
||||
.then(this.setObject);
|
||||
this.openmct.time.on("bounds", this.refreshData);
|
||||
|
||||
this.status = this.openmct.status.get(this.item.identifier);
|
||||
this.removeStatusListener = this.openmct.status.observe(this.item.identifier, this.setStatus);
|
||||
},
|
||||
destroyed() {
|
||||
this.removeSubscription();
|
||||
this.removeStatusListener();
|
||||
|
||||
if (this.removeSelectable) {
|
||||
this.removeSelectable();
|
||||
@ -216,6 +231,18 @@ export default {
|
||||
this.openmct.time.off("bounds", this.refreshData);
|
||||
},
|
||||
methods: {
|
||||
getViewContext() {
|
||||
return {
|
||||
getViewKey: () => this.viewKey,
|
||||
formattedValueForCopy: this.formattedValueForCopy
|
||||
};
|
||||
},
|
||||
formattedValueForCopy() {
|
||||
const timeFormatterKey = this.openmct.time.timeSystem().key;
|
||||
const timeFormatter = this.formats[timeFormatterKey];
|
||||
|
||||
return `At ${timeFormatter.format(this.datum)} ${this.domainObject.name} had a value of ${this.telemetryValue} ${this.unit}`;
|
||||
},
|
||||
requestHistoricalData() {
|
||||
let bounds = this.openmct.time.bounds();
|
||||
let options = {
|
||||
@ -253,6 +280,16 @@ export default {
|
||||
this.requestHistoricalData(this.domainObject);
|
||||
}
|
||||
},
|
||||
getView() {
|
||||
return {
|
||||
getViewContext() {
|
||||
return {
|
||||
viewHistoricalData: true,
|
||||
skipCache: true
|
||||
};
|
||||
}
|
||||
};
|
||||
},
|
||||
setObject(domainObject) {
|
||||
this.domainObject = domainObject;
|
||||
this.keyString = this.openmct.objects.makeKeyString(domainObject.identifier);
|
||||
@ -276,12 +313,40 @@ export default {
|
||||
this.removeSelectable = this.openmct.selection.selectable(
|
||||
this.$el, this.context, this.immediatelySelect || this.initSelect);
|
||||
delete this.immediatelySelect;
|
||||
|
||||
let allActions = this.openmct.actions.get(this.currentObjectPath, this.getView());
|
||||
|
||||
this.applicableActions = CONTEXT_MENU_ACTIONS.map(actionKey => {
|
||||
return allActions[actionKey];
|
||||
});
|
||||
},
|
||||
updateTelemetryFormat(format) {
|
||||
this.$emit('formatChanged', this.item, format);
|
||||
},
|
||||
showContextMenu(event) {
|
||||
this.openmct.contextMenu._showContextMenuForObjectPath(this.currentObjectPath, event.x, event.y, CONTEXT_MENU_ACTIONS);
|
||||
async getContextMenuActions() {
|
||||
const defaultNotebook = getDefaultNotebook();
|
||||
const domainObject = defaultNotebook && await this.openmct.objects.get(defaultNotebook.notebookMeta.identifier);
|
||||
|
||||
const actionsObject = this.openmct.actions.get(this.currentObjectPath, this.getViewContext(), { viewHistoricalData: true }).applicableActions;
|
||||
let applicableActionKeys = Object.keys(actionsObject)
|
||||
.filter(key => {
|
||||
const isCopyToNotebook = actionsObject[key].key === 'copyToNotebook';
|
||||
if (defaultNotebook && isCopyToNotebook) {
|
||||
const defaultPath = domainObject && `${domainObject.name} - ${defaultNotebook.section.name} - ${defaultNotebook.page.name}`;
|
||||
actionsObject[key].name = `Copy to Notebook ${defaultPath}`;
|
||||
}
|
||||
|
||||
return CONTEXT_MENU_ACTIONS.includes(actionsObject[key].key);
|
||||
});
|
||||
|
||||
return applicableActionKeys.map(key => actionsObject[key]);
|
||||
},
|
||||
async showContextMenu(event) {
|
||||
const contextMenuActions = await this.getContextMenuActions();
|
||||
this.openmct.menus.showMenu(event.x, event.y, contextMenuActions);
|
||||
},
|
||||
setStatus(status) {
|
||||
this.status = status;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -29,7 +29,9 @@
|
||||
@endMove="() => $emit('endMove')"
|
||||
>
|
||||
<div
|
||||
class="c-text-view"
|
||||
class="c-text-view u-style-receiver js-style-receiver"
|
||||
:data-font-size="item.fontSize"
|
||||
:data-font="item.font"
|
||||
:class="[styleClass]"
|
||||
:style="style"
|
||||
>
|
||||
@ -47,13 +49,14 @@ export default {
|
||||
return {
|
||||
fill: '',
|
||||
stroke: '',
|
||||
size: '13px',
|
||||
color: '',
|
||||
x: 1,
|
||||
y: 1,
|
||||
width: 10,
|
||||
height: 5,
|
||||
text: element.text
|
||||
text: element.text,
|
||||
fontSize: 'default',
|
||||
font: 'default'
|
||||
};
|
||||
},
|
||||
inject: ['openmct'],
|
||||
@ -84,8 +87,14 @@ export default {
|
||||
},
|
||||
computed: {
|
||||
style() {
|
||||
let size;
|
||||
//legacy size support
|
||||
if (!this.item.fontSize) {
|
||||
size = this.item.size;
|
||||
}
|
||||
|
||||
return Object.assign({
|
||||
fontSize: this.item.size
|
||||
size
|
||||
}, this.itemStyle);
|
||||
}
|
||||
},
|
||||
|
@ -17,10 +17,29 @@
|
||||
flex-direction: column;
|
||||
overflow: auto;
|
||||
|
||||
&__grid-holder {
|
||||
&__grid-holder,
|
||||
&__dimensions {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&__dimensions {
|
||||
$b: 1px dashed $editDimensionsColor;
|
||||
border-right: $b;
|
||||
border-bottom: $b;
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
|
||||
&-vals {
|
||||
$p: 2px;
|
||||
color: $editDimensionsColor;
|
||||
display: inline-block;
|
||||
font-style: italic;
|
||||
position: absolute;
|
||||
bottom: $p; right: $p;
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
|
||||
&__frame {
|
||||
position: absolute;
|
||||
}
|
||||
@ -34,6 +53,10 @@
|
||||
> .l-layout {
|
||||
background: $editUIGridColorBg;
|
||||
|
||||
> [class*="__dimensions"] {
|
||||
display: block;
|
||||
}
|
||||
|
||||
> [class*="__grid-holder"] {
|
||||
display: block;
|
||||
}
|
||||
@ -42,12 +65,16 @@
|
||||
}
|
||||
|
||||
.l-layout__frame {
|
||||
&[s-selected],
|
||||
&[s-selected]:not([multi-select="true"]),
|
||||
&[s-selected-parent] {
|
||||
// Display grid and allow edit marquee to display in nested layouts when editing
|
||||
> * > * > .l-layout + .allow-editing {
|
||||
> * > * > .l-layout.allow-editing {
|
||||
box-shadow: inset $editUIGridColorFg 0 0 2px 1px;
|
||||
|
||||
> [class*="__dimensions"] {
|
||||
display: block;
|
||||
}
|
||||
|
||||
> [class*='grid-holder'] {
|
||||
display: block;
|
||||
}
|
||||
|
@ -29,12 +29,12 @@
|
||||
|
||||
@include isMissing($absPos: true);
|
||||
|
||||
.is-missing__indicator {
|
||||
.is-status__indicator {
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
&.is-missing {
|
||||
&.is-status--missing {
|
||||
border: $borderMissing;
|
||||
}
|
||||
}
|
||||
|
@ -27,6 +27,7 @@ export default {
|
||||
inject: ['openmct'],
|
||||
data() {
|
||||
return {
|
||||
objectStyle: undefined,
|
||||
itemStyle: undefined,
|
||||
styleClass: ''
|
||||
};
|
||||
|
@ -26,9 +26,12 @@ import objectUtils from 'objectUtils';
|
||||
import DisplayLayoutType from './DisplayLayoutType.js';
|
||||
import DisplayLayoutToolbar from './DisplayLayoutToolbar.js';
|
||||
import AlphaNumericFormatViewProvider from './AlphanumericFormatViewProvider.js';
|
||||
import CopyToClipboardAction from './actions/CopyToClipboardAction';
|
||||
|
||||
export default function DisplayLayoutPlugin(options) {
|
||||
return function (openmct) {
|
||||
openmct.actions.register(new CopyToClipboardAction(openmct));
|
||||
|
||||
openmct.objectViews.addProvider({
|
||||
key: 'layout.view',
|
||||
canView: function (domainObject) {
|
||||
@ -72,7 +75,8 @@ export default function DisplayLayoutPlugin(options) {
|
||||
duplicateItem: component && component.$refs.displayLayout.duplicateItem,
|
||||
switchViewType: component && component.$refs.displayLayout.switchViewType,
|
||||
mergeMultipleTelemetryViews: component && component.$refs.displayLayout.mergeMultipleTelemetryViews,
|
||||
mergeMultipleOverlayPlots: component && component.$refs.displayLayout.mergeMultipleOverlayPlots
|
||||
mergeMultipleOverlayPlots: component && component.$refs.displayLayout.mergeMultipleOverlayPlots,
|
||||
toggleGrid: component && component.$refs.displayLayout.toggleGrid
|
||||
};
|
||||
},
|
||||
onEditModeChange: function (isEditing) {
|
||||
|
@ -340,6 +340,7 @@ describe('the plugin', function () {
|
||||
|
||||
it('provides controls including separators', () => {
|
||||
const displayLayoutToolbar = openmct.toolbars.get(selection);
|
||||
|
||||
expect(displayLayoutToolbar.length).toBe(9);
|
||||
});
|
||||
});
|
||||
|
@ -3,20 +3,26 @@
|
||||
@include userSelectNone();
|
||||
background: $colorFilterBg;
|
||||
color: $colorFilterFg;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 0.9em;
|
||||
margin-top: $interiorMarginSm;
|
||||
padding: 2px;
|
||||
text-transform: uppercase;
|
||||
|
||||
&:before {
|
||||
font-family: symbolsfont-12px;
|
||||
content: $glyph-icon-filter;
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
margin-right: $interiorMarginSm;
|
||||
}
|
||||
|
||||
&--mixed {
|
||||
.c-filter-indication__mixed {
|
||||
font-style: italic;
|
||||
}
|
||||
}
|
||||
|
||||
&__label {
|
||||
+ .c-filter-indication__label {
|
||||
&:before {
|
||||
content: ', ';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.c-filter-tree-item {
|
||||
|
@ -1,11 +1,10 @@
|
||||
<template>
|
||||
<a
|
||||
class="l-grid-view__item c-grid-item"
|
||||
:class="{
|
||||
:class="[{
|
||||
'is-alias': item.isAlias === true,
|
||||
'is-missing': item.model.status === 'missing',
|
||||
'c-grid-item--unknown': item.type.cssClass === undefined || item.type.cssClass.indexOf('unknown') !== -1
|
||||
}"
|
||||
}, statusClass]"
|
||||
:href="objectLink"
|
||||
>
|
||||
<div
|
||||
@ -27,8 +26,8 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="c-grid-item__controls">
|
||||
<div class="is-missing__indicator"
|
||||
title="This item is missing"
|
||||
<div class="is-status__indicator"
|
||||
title="This item is missing or suspect"
|
||||
></div>
|
||||
<div
|
||||
class="icon-people"
|
||||
@ -46,9 +45,10 @@
|
||||
<script>
|
||||
import contextMenuGesture from '../../../ui/mixins/context-menu-gesture';
|
||||
import objectLink from '../../../ui/mixins/object-link';
|
||||
import statusListener from './status-listener';
|
||||
|
||||
export default {
|
||||
mixins: [contextMenuGesture, objectLink],
|
||||
mixins: [contextMenuGesture, objectLink, statusListener],
|
||||
props: {
|
||||
item: {
|
||||
type: Object,
|
||||
|
@ -8,18 +8,18 @@
|
||||
<a
|
||||
ref="objectLink"
|
||||
class="c-object-label"
|
||||
:class="{ 'is-missing': item.model.status === 'missing' }"
|
||||
:class="[statusClass]"
|
||||
:href="objectLink"
|
||||
>
|
||||
<div
|
||||
class="c-object-label__type-icon c-list-item__type-icon"
|
||||
class="c-object-label__type-icon c-list-item__name__type-icon"
|
||||
:class="item.type.cssClass"
|
||||
>
|
||||
<span class="is-missing__indicator"
|
||||
title="This item is missing"
|
||||
<span class="is-status__indicator"
|
||||
title="This item is missing or suspect"
|
||||
></span>
|
||||
</div>
|
||||
<div class="c-object-label__name c-list-item__name">{{ item.model.name }}</div>
|
||||
<div class="c-object-label__name c-list-item__name__name">{{ item.model.name }}</div>
|
||||
</a>
|
||||
</td>
|
||||
<td class="c-list-item__type">
|
||||
@ -39,9 +39,10 @@
|
||||
import moment from 'moment';
|
||||
import contextMenuGesture from '../../../ui/mixins/context-menu-gesture';
|
||||
import objectLink from '../../../ui/mixins/object-link';
|
||||
import statusListener from './status-listener';
|
||||
|
||||
export default {
|
||||
mixins: [contextMenuGesture, objectLink],
|
||||
mixins: [contextMenuGesture, objectLink, statusListener],
|
||||
props: {
|
||||
item: {
|
||||
type: Object,
|
||||
|
@ -11,6 +11,8 @@
|
||||
|
||||
body.desktop & {
|
||||
flex-flow: row wrap;
|
||||
align-content: flex-start;
|
||||
|
||||
&__item {
|
||||
height: $gridItemDesk;
|
||||
width: $gridItemDesk;
|
||||
@ -41,7 +43,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
&.is-missing {
|
||||
&.is-status--missing {
|
||||
@include isMissing();
|
||||
|
||||
[class*='__type-icon'],
|
||||
|
@ -1,11 +1,19 @@
|
||||
/******************************* LIST ITEM */
|
||||
.c-list-item {
|
||||
&__type-icon {
|
||||
&__name__type-icon {
|
||||
color: $colorItemTreeIcon;
|
||||
}
|
||||
|
||||
&__name {
|
||||
&__name__name {
|
||||
@include ellipsize();
|
||||
|
||||
a & {
|
||||
color: $colorItemFg;
|
||||
}
|
||||
}
|
||||
|
||||
&:not(.c-list-item__name) {
|
||||
color: $colorItemFgDetails;
|
||||
}
|
||||
|
||||
&.is-alias {
|
||||
|
@ -28,9 +28,5 @@
|
||||
padding-top: $p;
|
||||
padding-bottom: $p;
|
||||
width: 25%;
|
||||
|
||||
&:not(.c-list-item__name) {
|
||||
color: $colorItemFgDetails;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
33
src/plugins/folderView/components/status-listener.js
Normal file
33
src/plugins/folderView/components/status-listener.js
Normal file
@ -0,0 +1,33 @@
|
||||
export default {
|
||||
inject: ['openmct'],
|
||||
props: {
|
||||
item: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
statusClass() {
|
||||
return (this.status) ? `is-status--${this.status}` : '';
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
status: ''
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
setStatus(status) {
|
||||
this.status = status;
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
let identifier = this.item.model.identifier;
|
||||
|
||||
this.status = this.openmct.status.get(identifier);
|
||||
this.removeStatusListener = this.openmct.status.observe(identifier, this.setStatus);
|
||||
},
|
||||
destroyed() {
|
||||
this.removeStatusListener();
|
||||
}
|
||||
};
|
@ -25,6 +25,8 @@ export default class GoToOriginalAction {
|
||||
this.name = 'Go To Original';
|
||||
this.key = 'goToOriginal';
|
||||
this.description = 'Go to the original unlinked instance of this object';
|
||||
this.group = 'action';
|
||||
this.priority = 4;
|
||||
|
||||
this._openmct = openmct;
|
||||
}
|
||||
|
@ -23,6 +23,6 @@ import GoToOriginalAction from './goToOriginalAction';
|
||||
|
||||
export default function () {
|
||||
return function (openmct) {
|
||||
openmct.contextMenu.registerAction(new GoToOriginalAction(openmct));
|
||||
openmct.actions.register(new GoToOriginalAction(openmct));
|
||||
};
|
||||
}
|
||||
|
@ -1,116 +1,224 @@
|
||||
<template>
|
||||
<div class="c-imagery">
|
||||
<div
|
||||
tabindex="0"
|
||||
class="c-imagery"
|
||||
@keyup="arrowUpHandler"
|
||||
@keydown="arrowDownHandler"
|
||||
@mouseover="focusElement"
|
||||
>
|
||||
<div class="c-imagery__main-image-wrapper has-local-controls">
|
||||
<div class="h-local-controls h-local-controls--overlay-content c-local-controls--show-on-hover l-flex-row c-imagery__lc">
|
||||
<span class="holder flex-elem grows c-imagery__lc__sliders">
|
||||
<input v-model="filters.brightness"
|
||||
class="icon-brightness"
|
||||
type="range"
|
||||
min="0"
|
||||
max="500"
|
||||
>
|
||||
<input v-model="filters.contrast"
|
||||
class="icon-contrast"
|
||||
type="range"
|
||||
min="0"
|
||||
max="500"
|
||||
>
|
||||
<div class="h-local-controls h-local-controls--overlay-content c-local-controls--show-on-hover c-image-controls__controls">
|
||||
<span class="c-image-controls__sliders"
|
||||
draggable="true"
|
||||
@dragstart="startDrag"
|
||||
>
|
||||
<div class="c-image-controls__slider-wrapper icon-brightness">
|
||||
<input v-model="filters.brightness"
|
||||
type="range"
|
||||
min="0"
|
||||
max="500"
|
||||
>
|
||||
</div>
|
||||
<div class="c-image-controls__slider-wrapper icon-contrast">
|
||||
<input v-model="filters.contrast"
|
||||
type="range"
|
||||
min="0"
|
||||
max="500"
|
||||
>
|
||||
</div>
|
||||
</span>
|
||||
<span class="holder flex-elem t-reset-btn-holder c-imagery__lc__reset-btn">
|
||||
<span class="t-reset-btn-holder c-imagery__lc__reset-btn c-image-controls__btn-reset">
|
||||
<a class="s-icon-button icon-reset t-btn-reset"
|
||||
@click="filters={brightness: 100, contrast: 100}"
|
||||
></a>
|
||||
</span>
|
||||
</div>
|
||||
<div class="main-image s-image-main c-imagery__main-image has-local-controls"
|
||||
:class="{'paused unnsynced': paused(),'stale':false }"
|
||||
:style="{'background-image': getImageUrl() ? `url(${getImageUrl()})` : 'none',
|
||||
'filter': `brightness(${filters.brightness}%) contrast(${filters.contrast}%)`}"
|
||||
<div class="c-imagery__main-image__bg"
|
||||
:class="{'paused unnsynced': isPaused,'stale':false }"
|
||||
>
|
||||
<div class="c-local-controls c-local-controls--show-on-hover c-imagery__prev-next-buttons">
|
||||
<button class="c-nav c-nav--prev"
|
||||
title="Previous image"
|
||||
:disabled="isPrevDisabled()"
|
||||
@click="prevImage()"
|
||||
></button>
|
||||
<button class="c-nav c-nav--next"
|
||||
title="Next image"
|
||||
:disabled="isNextDisabled()"
|
||||
@click="nextImage()"
|
||||
></button>
|
||||
</div>
|
||||
<div class="c-imagery__main-image__image"
|
||||
:style="{
|
||||
'background-image': imageUrl ? `url(${imageUrl})` : 'none',
|
||||
'filter': `brightness(${filters.brightness}%) contrast(${filters.contrast}%)`
|
||||
}"
|
||||
:data-openmct-image-timestamp="time"
|
||||
:data-openmct-object-keystring="keyString"
|
||||
></div>
|
||||
</div>
|
||||
<div class="c-local-controls c-local-controls--show-on-hover c-imagery__prev-next-buttons">
|
||||
<button class="c-nav c-nav--prev"
|
||||
title="Previous image"
|
||||
:disabled="isPrevDisabled"
|
||||
@click="prevImage()"
|
||||
></button>
|
||||
<button class="c-nav c-nav--next"
|
||||
title="Next image"
|
||||
:disabled="isNextDisabled"
|
||||
@click="nextImage()"
|
||||
></button>
|
||||
</div>
|
||||
|
||||
<div class="c-imagery__control-bar">
|
||||
<div class="c-imagery__timestamp">{{ getTime() }}</div>
|
||||
<div class="h-local-controls flex-elem">
|
||||
<div class="c-imagery__time">
|
||||
<div class="c-imagery__timestamp u-style-receiver js-style-receiver">{{ time }}</div>
|
||||
<div
|
||||
v-if="canTrackDuration"
|
||||
:class="{'c-imagery--new': isImageNew && !refreshCSS}"
|
||||
class="c-imagery__age icon-timer"
|
||||
>{{ formattedDuration }}</div>
|
||||
</div>
|
||||
<div class="h-local-controls">
|
||||
<button
|
||||
class="c-button icon-pause pause-play"
|
||||
:class="{'is-paused': paused()}"
|
||||
@click="paused(!paused(), true)"
|
||||
:class="{'is-paused': isPaused}"
|
||||
@click="paused(!isPaused, 'button')"
|
||||
></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div ref="thumbsWrapper"
|
||||
class="c-imagery__thumbs-wrapper"
|
||||
:class="{'is-paused': paused()}"
|
||||
:class="{'is-paused': isPaused}"
|
||||
@scroll="handleScroll"
|
||||
>
|
||||
<div v-for="(imageData, index) in imageHistory"
|
||||
:key="index"
|
||||
<div v-for="(datum, index) in imageHistory"
|
||||
:key="datum.url"
|
||||
class="c-imagery__thumb c-thumb"
|
||||
:class="{selected: imageData.selected}"
|
||||
@click="setSelectedImage(imageData)"
|
||||
:class="{ selected: focusedImageIndex === index && isPaused }"
|
||||
@click="setFocusedImage(index, thumbnailClick)"
|
||||
>
|
||||
<img class="c-thumb__image"
|
||||
:src="getImageUrl(imageData)"
|
||||
:src="formatImageUrl(datum)"
|
||||
>
|
||||
<div class="c-thumb__timestamp">{{ getTime(imageData) }}</div>
|
||||
<div class="c-thumb__timestamp">{{ formatTime(datum) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import moment from 'moment';
|
||||
|
||||
const DEFAULT_DURATION_FORMATTER = 'duration';
|
||||
const REFRESH_CSS_MS = 500;
|
||||
const DURATION_TRACK_MS = 1000;
|
||||
const ARROW_DOWN_DELAY_CHECK_MS = 400;
|
||||
const ARROW_SCROLL_RATE_MS = 100;
|
||||
const THUMBNAIL_CLICKED = true;
|
||||
|
||||
const ONE_MINUTE = 60 * 1000;
|
||||
const FIVE_MINUTES = 5 * ONE_MINUTE;
|
||||
const ONE_HOUR = ONE_MINUTE * 60;
|
||||
const EIGHT_HOURS = 8 * ONE_HOUR;
|
||||
const TWENTYFOUR_HOURS = EIGHT_HOURS * 3;
|
||||
|
||||
const ARROW_RIGHT = 39;
|
||||
const ARROW_LEFT = 37;
|
||||
|
||||
export default {
|
||||
inject: ['openmct', 'domainObject'],
|
||||
data() {
|
||||
let timeSystem = this.openmct.time.timeSystem();
|
||||
|
||||
return {
|
||||
autoScroll: true,
|
||||
durationFormatter: undefined,
|
||||
filters: {
|
||||
brightness: 100,
|
||||
contrast: 100
|
||||
},
|
||||
image: {
|
||||
selected: ''
|
||||
},
|
||||
imageFormat: '',
|
||||
imageHistory: [],
|
||||
imageUrl: '',
|
||||
thumbnailClick: THUMBNAIL_CLICKED,
|
||||
isPaused: false,
|
||||
metadata: {},
|
||||
requestCount: 0,
|
||||
timeFormat: ''
|
||||
timeSystem: timeSystem,
|
||||
timeFormatter: undefined,
|
||||
refreshCSS: false,
|
||||
keyString: undefined,
|
||||
focusedImageIndex: undefined,
|
||||
numericDuration: undefined
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
bounds() {
|
||||
return this.openmct.time.bounds();
|
||||
time() {
|
||||
return this.formatTime(this.focusedImage);
|
||||
},
|
||||
imageUrl() {
|
||||
return this.formatImageUrl(this.focusedImage);
|
||||
},
|
||||
isImageNew() {
|
||||
let cutoff = FIVE_MINUTES;
|
||||
let age = this.numericDuration;
|
||||
|
||||
return age < cutoff && !this.refreshCSS;
|
||||
},
|
||||
canTrackDuration() {
|
||||
return this.openmct.time.clock() && this.timeSystem.isUTCBased;
|
||||
},
|
||||
isNextDisabled() {
|
||||
let disabled = false;
|
||||
|
||||
if (this.focusedImageIndex === -1 || this.focusedImageIndex === this.imageHistory.length - 1) {
|
||||
disabled = true;
|
||||
}
|
||||
|
||||
return disabled;
|
||||
},
|
||||
isPrevDisabled() {
|
||||
let disabled = false;
|
||||
|
||||
if (this.focusedImageIndex === 0 || this.imageHistory.length < 2) {
|
||||
disabled = true;
|
||||
}
|
||||
|
||||
return disabled;
|
||||
},
|
||||
focusedImage() {
|
||||
return this.imageHistory[this.focusedImageIndex];
|
||||
},
|
||||
parsedSelectedTime() {
|
||||
return this.parseTime(this.focusedImage);
|
||||
},
|
||||
formattedDuration() {
|
||||
let result = 'N/A';
|
||||
let negativeAge = -1;
|
||||
|
||||
if (this.numericDuration > TWENTYFOUR_HOURS) {
|
||||
negativeAge *= (this.numericDuration / TWENTYFOUR_HOURS);
|
||||
result = moment.duration(negativeAge, 'days').humanize(true);
|
||||
} else if (this.numericDuration > EIGHT_HOURS) {
|
||||
negativeAge *= (this.numericDuration / ONE_HOUR);
|
||||
result = moment.duration(negativeAge, 'hours').humanize(true);
|
||||
} else if (this.durationFormatter) {
|
||||
result = this.durationFormatter.format(this.numericDuration);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
focusedImageIndex() {
|
||||
this.trackDuration();
|
||||
this.resetAgeCSS();
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
// set
|
||||
this.keystring = this.openmct.objects.makeKeyString(this.domainObject.identifier);
|
||||
this.metadata = this.openmct.telemetry.getMetadata(this.domainObject);
|
||||
this.imageFormat = this.openmct.telemetry.getValueFormatter(this.metadata.valuesForHints(['image'])[0]);
|
||||
// initialize
|
||||
this.timeKey = this.openmct.time.timeSystem().key;
|
||||
this.timeFormat = this.openmct.telemetry.getValueFormatter(this.metadata.value(this.timeKey));
|
||||
// listen
|
||||
this.openmct.time.on('bounds', this.boundsChange);
|
||||
this.openmct.time.on('timeSystem', this.timeSystemChange);
|
||||
this.openmct.time.on('clock', this.clockChange);
|
||||
|
||||
// set
|
||||
this.keyString = this.openmct.objects.makeKeyString(this.domainObject.identifier);
|
||||
this.metadata = this.openmct.telemetry.getMetadata(this.domainObject);
|
||||
this.durationFormatter = this.getFormatter(this.timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER);
|
||||
this.imageFormatter = this.openmct.telemetry.getValueFormatter(this.metadata.valuesForHints(['image'])[0]);
|
||||
|
||||
// initialize
|
||||
this.timeKey = this.timeSystem.key;
|
||||
this.timeFormatter = this.getFormatter(this.timeKey);
|
||||
|
||||
// kickoff
|
||||
this.subscribe();
|
||||
this.requestHistory();
|
||||
@ -124,38 +232,55 @@ export default {
|
||||
delete this.unsubscribe;
|
||||
}
|
||||
|
||||
this.stopDurationTracking();
|
||||
this.openmct.time.off('bounds', this.boundsChange);
|
||||
this.openmct.time.off('timeSystem', this.timeSystemChange);
|
||||
this.openmct.time.off('clock', this.clockChange);
|
||||
},
|
||||
methods: {
|
||||
focusElement() {
|
||||
this.$el.focus();
|
||||
},
|
||||
datumIsNotValid(datum) {
|
||||
if (this.imageHistory.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const datumTime = this.timeFormat.format(datum);
|
||||
const datumURL = this.imageFormat.format(datum);
|
||||
const lastHistoryTime = this.timeFormat.format(this.imageHistory.slice(-1)[0]);
|
||||
const lastHistoryURL = this.imageFormat.format(this.imageHistory.slice(-1)[0]);
|
||||
const datumURL = this.formatImageUrl(datum);
|
||||
const lastHistoryURL = this.formatImageUrl(this.imageHistory.slice(-1)[0]);
|
||||
|
||||
// datum is not valid if it matches the last datum in history,
|
||||
// or it is before the last datum in the history
|
||||
const datumTimeCheck = this.timeFormat.parse(datum);
|
||||
const historyTimeCheck = this.timeFormat.parse(this.imageHistory.slice(-1)[0]);
|
||||
const matchesLast = (datumTime === lastHistoryTime) && (datumURL === lastHistoryURL);
|
||||
const datumTimeCheck = this.parseTime(datum);
|
||||
const historyTimeCheck = this.parseTime(this.imageHistory.slice(-1)[0]);
|
||||
const matchesLast = (datumTimeCheck === historyTimeCheck) && (datumURL === lastHistoryURL);
|
||||
const isStale = datumTimeCheck < historyTimeCheck;
|
||||
|
||||
return matchesLast || isStale;
|
||||
},
|
||||
getImageUrl(datum) {
|
||||
return datum
|
||||
? this.imageFormat.format(datum)
|
||||
: this.imageUrl;
|
||||
formatImageUrl(datum) {
|
||||
if (!datum) {
|
||||
return;
|
||||
}
|
||||
|
||||
return this.imageFormatter.format(datum);
|
||||
},
|
||||
getTime(datum) {
|
||||
return datum
|
||||
? this.timeFormat.format(datum)
|
||||
: this.time;
|
||||
formatTime(datum) {
|
||||
if (!datum) {
|
||||
return;
|
||||
}
|
||||
|
||||
let dateTimeStr = this.timeFormatter.format(datum);
|
||||
|
||||
// Replace ISO "T" with a space to allow wrapping
|
||||
return dateTimeStr.replace("T", " ");
|
||||
},
|
||||
parseTime(datum) {
|
||||
if (!datum) {
|
||||
return;
|
||||
}
|
||||
|
||||
return this.timeFormatter.parse(datum);
|
||||
},
|
||||
handleScroll() {
|
||||
const thumbsWrapper = this.$refs.thumbsWrapper;
|
||||
@ -168,26 +293,35 @@ export default {
|
||||
|| (scrollHeight - scrollTop) > 2 * clientHeight;
|
||||
this.autoScroll = !disableScroll;
|
||||
},
|
||||
paused(state, button = false) {
|
||||
if (arguments.length > 0 && state !== this.isPaused) {
|
||||
this.unselectAllImages();
|
||||
this.isPaused = state;
|
||||
if (state === true && button) {
|
||||
// If we are pausing, select the latest image in imageHistory
|
||||
this.setSelectedImage(this.imageHistory[this.imageHistory.length - 1]);
|
||||
}
|
||||
paused(state, type) {
|
||||
|
||||
if (this.nextDatum) {
|
||||
this.updateValues(this.nextDatum);
|
||||
delete this.nextDatum;
|
||||
} else {
|
||||
this.updateValues(this.imageHistory[this.imageHistory.length - 1]);
|
||||
}
|
||||
this.isPaused = state;
|
||||
|
||||
this.autoScroll = true;
|
||||
if (type === 'button') {
|
||||
this.setFocusedImage(this.imageHistory.length - 1);
|
||||
}
|
||||
|
||||
return this.isPaused;
|
||||
if (this.nextImageIndex) {
|
||||
this.setFocusedImage(this.nextImageIndex);
|
||||
delete this.nextImageIndex;
|
||||
}
|
||||
|
||||
this.autoScroll = true;
|
||||
},
|
||||
scrollToFocused() {
|
||||
const thumbsWrapper = this.$refs.thumbsWrapper;
|
||||
if (!thumbsWrapper) {
|
||||
return;
|
||||
}
|
||||
|
||||
let domThumb = thumbsWrapper.children[this.focusedImageIndex];
|
||||
|
||||
if (domThumb) {
|
||||
domThumb.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'center'
|
||||
});
|
||||
}
|
||||
},
|
||||
scrollToRight() {
|
||||
if (this.isPaused || !this.$refs.thumbsWrapper || !this.autoScroll) {
|
||||
@ -201,22 +335,17 @@ export default {
|
||||
|
||||
setTimeout(() => this.$refs.thumbsWrapper.scrollLeft = scrollWidth, 0);
|
||||
},
|
||||
setSelectedImage(image) {
|
||||
// If we are paused and the current image IS selected, unpause
|
||||
// Otherwise, set current image and pause
|
||||
if (!image) {
|
||||
setFocusedImage(index, thumbnailClick = false) {
|
||||
if (this.isPaused && !thumbnailClick) {
|
||||
this.nextImageIndex = index;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.isPaused && image.selected) {
|
||||
this.paused(false);
|
||||
this.unselectAllImages();
|
||||
} else {
|
||||
this.imageUrl = this.getImageUrl(image);
|
||||
this.time = this.getTime(image);
|
||||
this.focusedImageIndex = index;
|
||||
|
||||
if (thumbnailClick && !this.isPaused) {
|
||||
this.paused(true);
|
||||
this.unselectAllImages();
|
||||
image.selected = true;
|
||||
}
|
||||
},
|
||||
boundsChange(bounds, isTick) {
|
||||
@ -224,98 +353,162 @@ export default {
|
||||
this.requestHistory();
|
||||
}
|
||||
},
|
||||
requestHistory() {
|
||||
const requestId = ++this.requestCount;
|
||||
async requestHistory() {
|
||||
let bounds = this.openmct.time.bounds();
|
||||
this.requestCount++;
|
||||
const requestId = this.requestCount;
|
||||
this.imageHistory = [];
|
||||
this.openmct.telemetry
|
||||
.request(this.domainObject, this.bounds)
|
||||
.then((values = []) => {
|
||||
if (this.requestCount === requestId) {
|
||||
// add each image to the history
|
||||
// update values for the very last image (set current image time and url)
|
||||
values.forEach((datum, index) => this.updateHistory(datum, index === values.length - 1));
|
||||
}
|
||||
let data = await this.openmct.telemetry
|
||||
.request(this.domainObject, bounds) || [];
|
||||
|
||||
if (this.requestCount === requestId) {
|
||||
data.forEach((datum, index) => {
|
||||
this.updateHistory(datum, index === data.length - 1);
|
||||
});
|
||||
}
|
||||
},
|
||||
timeSystemChange(system) {
|
||||
// reset timesystem dependent variables
|
||||
this.timeKey = system.key;
|
||||
this.timeFormat = this.openmct.telemetry.getValueFormatter(this.metadata.value(this.timeKey));
|
||||
this.timeSystem = this.openmct.time.timeSystem();
|
||||
this.timeKey = this.timeSystem.key;
|
||||
this.timeFormatter = this.getFormatter(this.timeKey);
|
||||
this.durationFormatter = this.getFormatter(this.timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER);
|
||||
this.trackDuration();
|
||||
},
|
||||
clockChange(clock) {
|
||||
this.trackDuration();
|
||||
},
|
||||
subscribe() {
|
||||
this.unsubscribe = this.openmct.telemetry
|
||||
.subscribe(this.domainObject, (datum) => {
|
||||
let parsedTimestamp = this.timeFormat.parse(datum);
|
||||
let parsedTimestamp = this.parseTime(datum);
|
||||
let bounds = this.openmct.time.bounds();
|
||||
|
||||
if (parsedTimestamp >= this.bounds.start && parsedTimestamp <= this.bounds.end) {
|
||||
if (parsedTimestamp >= bounds.start && parsedTimestamp <= bounds.end) {
|
||||
this.updateHistory(datum);
|
||||
}
|
||||
});
|
||||
},
|
||||
unselectAllImages() {
|
||||
this.imageHistory.forEach(image => image.selected = false);
|
||||
},
|
||||
updateHistory(datum, updateValues = true) {
|
||||
updateHistory(datum, setFocused = true) {
|
||||
if (this.datumIsNotValid(datum)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.imageHistory.push(datum);
|
||||
|
||||
if (updateValues) {
|
||||
this.updateValues(datum);
|
||||
if (setFocused) {
|
||||
this.setFocusedImage(this.imageHistory.length - 1);
|
||||
}
|
||||
},
|
||||
updateValues(datum) {
|
||||
if (this.isPaused) {
|
||||
this.nextDatum = datum;
|
||||
getFormatter(key) {
|
||||
let metadataValue = this.metadata.value(key) || { format: key };
|
||||
let valueFormatter = this.openmct.telemetry.getValueFormatter(metadataValue);
|
||||
|
||||
return valueFormatter;
|
||||
},
|
||||
trackDuration() {
|
||||
if (this.canTrackDuration) {
|
||||
this.stopDurationTracking();
|
||||
this.updateDuration();
|
||||
this.durationTracker = window.setInterval(
|
||||
this.updateDuration, DURATION_TRACK_MS
|
||||
);
|
||||
} else {
|
||||
this.stopDurationTracking();
|
||||
}
|
||||
},
|
||||
stopDurationTracking() {
|
||||
window.clearInterval(this.durationTracker);
|
||||
},
|
||||
updateDuration() {
|
||||
let currentTime = this.openmct.time.clock().currentValue();
|
||||
this.numericDuration = currentTime - this.parsedSelectedTime;
|
||||
},
|
||||
resetAgeCSS() {
|
||||
this.refreshCSS = true;
|
||||
// unable to make this work with nextTick
|
||||
setTimeout(() => {
|
||||
this.refreshCSS = false;
|
||||
}, REFRESH_CSS_MS);
|
||||
},
|
||||
nextImage() {
|
||||
if (this.isNextDisabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.time = this.timeFormat.format(datum);
|
||||
this.imageUrl = this.imageFormat.format(datum);
|
||||
},
|
||||
selectedImageIndex() {
|
||||
return this.imageHistory.findIndex(image => image.selected);
|
||||
},
|
||||
setSelectedByIndex(index) {
|
||||
this.setSelectedImage(this.imageHistory[index]);
|
||||
},
|
||||
nextImage() {
|
||||
let index = this.selectedImageIndex();
|
||||
this.setSelectedByIndex(++index);
|
||||
let index = this.focusedImageIndex;
|
||||
|
||||
this.setFocusedImage(++index, THUMBNAIL_CLICKED);
|
||||
if (index === this.imageHistory.length - 1) {
|
||||
this.paused(false);
|
||||
}
|
||||
},
|
||||
prevImage() {
|
||||
let index = this.selectedImageIndex();
|
||||
if (index === -1) {
|
||||
this.setSelectedByIndex(this.imageHistory.length - 2);
|
||||
if (this.isPrevDisabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
let index = this.focusedImageIndex;
|
||||
|
||||
if (index === this.imageHistory.length - 1) {
|
||||
this.setFocusedImage(this.imageHistory.length - 2, THUMBNAIL_CLICKED);
|
||||
} else {
|
||||
this.setSelectedByIndex(--index);
|
||||
this.setFocusedImage(--index, THUMBNAIL_CLICKED);
|
||||
}
|
||||
},
|
||||
isNextDisabled() {
|
||||
let disabled = false;
|
||||
let index = this.selectedImageIndex();
|
||||
|
||||
if (index === -1 || index === this.imageHistory.length - 1) {
|
||||
disabled = true;
|
||||
}
|
||||
|
||||
return disabled;
|
||||
startDrag(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
},
|
||||
isPrevDisabled() {
|
||||
let disabled = false;
|
||||
let index = this.selectedImageIndex();
|
||||
arrowDownHandler(event) {
|
||||
let key = event.keyCode;
|
||||
|
||||
if (index === 0 || this.imageHistory.length < 2) {
|
||||
disabled = true;
|
||||
if (this.isLeftOrRightArrowKey(key)) {
|
||||
this.arrowDown = true;
|
||||
window.clearTimeout(this.arrowDownDelayTimeout);
|
||||
this.arrowDownDelayTimeout = window.setTimeout(() => {
|
||||
this.arrowKeyScroll(this.directionByKey(key));
|
||||
}, ARROW_DOWN_DELAY_CHECK_MS);
|
||||
}
|
||||
},
|
||||
arrowUpHandler(event) {
|
||||
let key = event.keyCode;
|
||||
|
||||
window.clearTimeout(this.arrowDownDelayTimeout);
|
||||
|
||||
if (this.isLeftOrRightArrowKey(key)) {
|
||||
this.arrowDown = false;
|
||||
let direction = this.directionByKey(key);
|
||||
this[direction + 'Image']();
|
||||
}
|
||||
},
|
||||
arrowKeyScroll(direction) {
|
||||
if (this.arrowDown) {
|
||||
this.arrowKeyScrolling = true;
|
||||
this[direction + 'Image']();
|
||||
setTimeout(() => {
|
||||
this.arrowKeyScroll(direction);
|
||||
}, ARROW_SCROLL_RATE_MS);
|
||||
} else {
|
||||
window.clearTimeout(this.arrowDownDelayTimeout);
|
||||
this.arrowKeyScrolling = false;
|
||||
this.scrollToFocused();
|
||||
}
|
||||
},
|
||||
directionByKey(keyCode) {
|
||||
let direction;
|
||||
|
||||
if (keyCode === ARROW_LEFT) {
|
||||
direction = 'prev';
|
||||
}
|
||||
|
||||
return disabled;
|
||||
if (keyCode === ARROW_RIGHT) {
|
||||
direction = 'next';
|
||||
}
|
||||
|
||||
return direction;
|
||||
},
|
||||
isLeftOrRightArrowKey(keyCode) {
|
||||
return [ARROW_RIGHT, ARROW_LEFT].includes(keyCode);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -1,8 +1,12 @@
|
||||
.c-imagery {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1 1 auto;
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
> * + * {
|
||||
margin-top: $interiorMargin;
|
||||
@ -15,24 +19,75 @@
|
||||
}
|
||||
|
||||
&__main-image {
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: contain;
|
||||
height: 100%;
|
||||
&__bg {
|
||||
background-color: $colorPlotBg;
|
||||
border: 1px solid transparent;
|
||||
flex: 1 1 auto;
|
||||
|
||||
&.unnsynced{
|
||||
@include sUnsynced();
|
||||
&.unnsynced{
|
||||
@include sUnsynced();
|
||||
}
|
||||
}
|
||||
|
||||
&__image {
|
||||
@include abs(); // Safari fix
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: contain;
|
||||
}
|
||||
}
|
||||
|
||||
&__control-bar {
|
||||
padding: 5px 0 0 0;
|
||||
&__control-bar,
|
||||
&__time {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
align-items: baseline;
|
||||
|
||||
> * + * {
|
||||
margin-left: $interiorMarginSm;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
&__control-bar {
|
||||
margin-top: 2px;
|
||||
padding: $interiorMarginSm 0;
|
||||
justify-content: space-between;
|
||||
|
||||
}
|
||||
|
||||
&__time {
|
||||
flex: 0 1 auto;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
&__timestamp,
|
||||
&__age {
|
||||
@include ellipsize();
|
||||
flex: 0 1 auto;
|
||||
}
|
||||
|
||||
&__timestamp {
|
||||
flex: 1 1 auto;
|
||||
flex-shrink: 10;
|
||||
}
|
||||
|
||||
&__age {
|
||||
border-radius: $controlCr;
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
align-items: baseline;
|
||||
padding: 1px $interiorMarginSm;
|
||||
|
||||
&:before {
|
||||
opacity: 0.5;
|
||||
margin-right: $interiorMarginSm;
|
||||
}
|
||||
}
|
||||
|
||||
&--new {
|
||||
// New imagery
|
||||
$bgColor: $colorOk;
|
||||
background: rgba($bgColor, 0.5);
|
||||
@include flash($animName: flashImageAge, $dur: 250ms, $valStart: rgba($colorOk, 0.7), $valEnd: rgba($colorOk, 0));
|
||||
}
|
||||
|
||||
&__thumbs-wrapper {
|
||||
@ -91,11 +146,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.s-image-main {
|
||||
background-color: $colorPlotBg;
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
/*************************************** IMAGERY LOCAL CONTROLS*/
|
||||
.c-imagery {
|
||||
.h-local-controls--overlay-content {
|
||||
@ -105,7 +155,7 @@
|
||||
background: $colorLocalControlOvrBg;
|
||||
border-radius: $basicCr;
|
||||
max-width: 200px;
|
||||
min-width: 100px;
|
||||
min-width: 70px;
|
||||
width: 35%;
|
||||
align-items: center;
|
||||
padding: $interiorMargin $interiorMarginLg;
|
||||
@ -126,6 +176,7 @@
|
||||
&__lc {
|
||||
&__reset-btn {
|
||||
$bc: $scrollbarTrackColorBg;
|
||||
|
||||
&:before,
|
||||
&:after {
|
||||
border-right: 1px solid $bc;
|
||||
@ -148,9 +199,51 @@
|
||||
}
|
||||
}
|
||||
|
||||
.c-image-controls {
|
||||
// Brightness/contrast
|
||||
|
||||
&__controls {
|
||||
// Sliders and reset element
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-right: $interiorMargin; // Need some extra space due to proximity to close button
|
||||
}
|
||||
|
||||
&__sliders {
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
flex-direction: column;
|
||||
|
||||
> * + * {
|
||||
margin-top: 11px;
|
||||
}
|
||||
}
|
||||
|
||||
&__slider-wrapper {
|
||||
// A wrapper is needed to add the type icon to left of each range input
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
&:before {
|
||||
color: rgba($colorMenuFg, 0.5);
|
||||
margin-right: $interiorMarginSm;
|
||||
}
|
||||
|
||||
input[type='range'] {
|
||||
width: 100px;
|
||||
}
|
||||
}
|
||||
|
||||
&__btn-reset {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
}
|
||||
|
||||
/*************************************** BUTTONS */
|
||||
.c-button.pause-play {
|
||||
// Pause icon set by default in markup
|
||||
justify-self: end;
|
||||
|
||||
&.is-paused {
|
||||
background: $colorPausedBg !important;
|
||||
color: $colorPausedFg;
|
||||
@ -162,14 +255,13 @@
|
||||
}
|
||||
|
||||
.c-imagery__prev-next-buttons {
|
||||
//background: rgba(deeppink, 0.2);
|
||||
display: flex;
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
transform: translateY(-75%);
|
||||
|
||||
.c-nav {
|
||||
pointer-events: all;
|
||||
|
@ -28,6 +28,8 @@ export default class NewFolderAction {
|
||||
this.key = 'newFolder';
|
||||
this.description = 'Create a new folder';
|
||||
this.cssClass = 'icon-folder-new';
|
||||
this.group = "action";
|
||||
this.priority = 9;
|
||||
|
||||
this._openmct = openmct;
|
||||
this._dialogForm = {
|
||||
|
@ -23,6 +23,6 @@ import NewFolderAction from './newFolderAction';
|
||||
|
||||
export default function () {
|
||||
return function (openmct) {
|
||||
openmct.contextMenu.registerAction(new NewFolderAction(openmct));
|
||||
openmct.actions.register(new NewFolderAction(openmct));
|
||||
};
|
||||
}
|
||||
|
@ -40,9 +40,7 @@ describe("the plugin", () => {
|
||||
openmct.on('start', done);
|
||||
openmct.startHeadless();
|
||||
|
||||
newFolderAction = openmct.contextMenu._allActions.filter(action => {
|
||||
return action.key === 'newFolder';
|
||||
})[0];
|
||||
newFolderAction = openmct.actions._allActions.newFolder;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
39
src/plugins/notebook/actions/CopyToNotebookAction.js
Normal file
39
src/plugins/notebook/actions/CopyToNotebookAction.js
Normal file
@ -0,0 +1,39 @@
|
||||
import { getDefaultNotebook } from '../utils/notebook-storage';
|
||||
import { addNotebookEntry } from '../utils/notebook-entries';
|
||||
|
||||
export default class CopyToNotebookAction {
|
||||
constructor(openmct) {
|
||||
this.openmct = openmct;
|
||||
|
||||
this.cssClass = 'icon-duplicate';
|
||||
this.description = 'Copy to Notebook action';
|
||||
this.group = "action";
|
||||
this.key = 'copyToNotebook';
|
||||
this.name = 'Copy to Notebook';
|
||||
this.priority = 9;
|
||||
}
|
||||
|
||||
copyToNotebook(entryText) {
|
||||
const notebookStorage = getDefaultNotebook();
|
||||
this.openmct.objects.get(notebookStorage.notebookMeta.identifier)
|
||||
.then(domainObject => {
|
||||
addNotebookEntry(this.openmct, domainObject, notebookStorage, null, entryText);
|
||||
|
||||
const defaultPath = `${domainObject.name} - ${notebookStorage.section.name} - ${notebookStorage.page.name}`;
|
||||
const msg = `Saved to Notebook ${defaultPath}`;
|
||||
this.openmct.notifications.info(msg);
|
||||
});
|
||||
}
|
||||
|
||||
invoke(objectPath, viewContext) {
|
||||
this.copyToNotebook(viewContext.formattedValueForCopy());
|
||||
}
|
||||
|
||||
appliesTo(objectPath, viewContext) {
|
||||
if (viewContext && viewContext.getViewKey) {
|
||||
return viewContext.getViewKey().includes('alphanumeric-format');
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
@ -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,15 +106,15 @@
|
||||
</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';
|
||||
import objectUtils from 'objectUtils';
|
||||
|
||||
const DEFAULT_CLASS = 'is-notebook-default';
|
||||
import { throttle } from 'lodash';
|
||||
|
||||
export default {
|
||||
inject: ['openmct', 'domainObject', 'snapshotContainer'],
|
||||
@ -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);
|
||||
},
|
||||
@ -195,15 +197,6 @@ export default {
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
addDefaultClass() {
|
||||
const classList = this.internalDomainObject.classList || [];
|
||||
if (classList.includes(DEFAULT_CLASS)) {
|
||||
return;
|
||||
}
|
||||
|
||||
classList.push(DEFAULT_CLASS);
|
||||
this.mutateObject('classList', classList);
|
||||
},
|
||||
changeSelectedSection({ sectionId, pageId }) {
|
||||
const sections = this.sections.map(s => {
|
||||
s.isSelected = false;
|
||||
@ -259,7 +252,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);
|
||||
@ -423,14 +416,7 @@ export default {
|
||||
return;
|
||||
}
|
||||
|
||||
const classList = domainObject.classList || [];
|
||||
const index = classList.indexOf(DEFAULT_CLASS);
|
||||
if (!classList.length || index < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
classList.splice(index, 1);
|
||||
this.openmct.objects.mutate(domainObject, 'classList', classList);
|
||||
this.openmct.status.delete(domainObject.identifier);
|
||||
},
|
||||
searchItem(input) {
|
||||
this.search = input;
|
||||
@ -440,11 +426,20 @@ export default {
|
||||
},
|
||||
async updateDefaultNotebook(notebookStorage) {
|
||||
const defaultNotebookObject = await this.getDefaultNotebookObject();
|
||||
this.removeDefaultClass(defaultNotebookObject);
|
||||
setDefaultNotebook(this.openmct, notebookStorage);
|
||||
this.addDefaultClass();
|
||||
this.defaultSectionId = notebookStorage.section.id;
|
||||
this.defaultPageId = notebookStorage.page.id;
|
||||
if (!defaultNotebookObject) {
|
||||
setDefaultNotebook(this.openmct, notebookStorage);
|
||||
} else if (objectUtils.makeKeyString(defaultNotebookObject.identifier) !== objectUtils.makeKeyString(notebookStorage.notebookMeta.identifier)) {
|
||||
this.removeDefaultClass(defaultNotebookObject);
|
||||
setDefaultNotebook(this.openmct, notebookStorage);
|
||||
}
|
||||
|
||||
if (this.defaultSectionId.length === 0 || this.defaultSectionId !== notebookStorage.section.id) {
|
||||
this.defaultSectionId = notebookStorage.section.id;
|
||||
}
|
||||
|
||||
if (this.defaultPageId.length === 0 || this.defaultPageId !== notebookStorage.page.id) {
|
||||
this.defaultPageId = notebookStorage.page.id;
|
||||
}
|
||||
},
|
||||
updateDefaultNotebookPage(pages, id) {
|
||||
if (!id) {
|
@ -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
|
||||
@ -191,7 +143,8 @@ export default {
|
||||
this.openmct.notifications.alert(message);
|
||||
}
|
||||
|
||||
window.location.href = link;
|
||||
const url = new URL(link);
|
||||
window.location.href = url.hash;
|
||||
},
|
||||
formatTime(unixTime, timeFormat) {
|
||||
return Moment.utc(unixTime).format(timeFormat);
|
||||
@ -209,7 +162,8 @@ export default {
|
||||
this.snapshot = new Vue({
|
||||
data: () => {
|
||||
return {
|
||||
embed: self.embed
|
||||
createdOn: this.createdOn,
|
||||
embed: this.embed
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
@ -218,13 +172,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 +184,7 @@ export default {
|
||||
label: 'Done',
|
||||
emphasis: true,
|
||||
callback: () => {
|
||||
snapshotOverlay.dismiss();
|
||||
this.snapshotOverlay.dismiss();
|
||||
}
|
||||
}
|
||||
]
|
||||
@ -262,6 +214,10 @@ export default {
|
||||
},
|
||||
updateEmbed(embed) {
|
||||
this.$emit('updateEmbed', embed);
|
||||
},
|
||||
updateSnapshot(snapshotObject) {
|
||||
this.embed.snapshot = snapshotObject;
|
||||
this.updateEmbed(this.embed);
|
||||
}
|
||||
}
|
||||
};
|
@ -1,23 +1,22 @@
|
||||
<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"
|
||||
class="c-ne__text"
|
||||
:class="{'c-input-inline' : !readOnly }"
|
||||
:class="{'c-ne__input' : !readOnly }"
|
||||
:contenteditable="!readOnly"
|
||||
:style="!entry.text.length ? defaultEntryStyle : ''"
|
||||
@blur="textBlur($event, entry.id)"
|
||||
@focus="textFocus($event, entry.id)"
|
||||
>{{ entry.text.length ? entry.text : defaultText }}</div>
|
||||
@blur="updateEntryValue($event, entry.id)"
|
||||
@focus="updateCurrentEntryValue($event, entry.id)"
|
||||
>{{ entry.text }}</div>
|
||||
<div class="c-snapshots c-ne__embeds">
|
||||
<NotebookEmbed v-for="embed in entry.embeds"
|
||||
:key="embed.id"
|
||||
@ -57,7 +56,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';
|
||||
|
||||
@ -106,37 +105,35 @@ export default {
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
currentEntryValue: '',
|
||||
defaultEntryStyle: {
|
||||
fontStyle: 'italic',
|
||||
color: '#6e6e6e'
|
||||
},
|
||||
defaultText: 'add description'
|
||||
currentEntryValue: ''
|
||||
};
|
||||
},
|
||||
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 +148,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 +161,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 +173,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,
|
||||
@ -246,14 +229,40 @@ export default {
|
||||
this.entry.embeds.splice(embedPosition, 1);
|
||||
this.updateEntry(this.entry);
|
||||
},
|
||||
selectTextInsideElement(element) {
|
||||
const range = document.createRange();
|
||||
range.selectNodeContents(element);
|
||||
let selection = window.getSelection();
|
||||
selection.removeAllRanges();
|
||||
selection.addRange(range);
|
||||
updateCurrentEntryValue($event) {
|
||||
if (this.readOnly) {
|
||||
return;
|
||||
}
|
||||
|
||||
const target = $event.target;
|
||||
this.currentEntryValue = target ? target.textContent : '';
|
||||
},
|
||||
textBlur($event, entryId) {
|
||||
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;
|
||||
}
|
||||
@ -266,48 +275,14 @@ export default {
|
||||
const entryPos = this.entryPosById(entryId);
|
||||
const value = target.textContent.trim();
|
||||
if (this.currentEntryValue !== value) {
|
||||
target.textContent = value;
|
||||
|
||||
const entries = getNotebookEntries(this.domainObject, this.selectedSection, this.selectedPage);
|
||||
entries[entryPos].text = value;
|
||||
|
||||
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);
|
||||
}
|
@ -1,29 +1,17 @@
|
||||
<template>
|
||||
<div class="c-menu-button c-ctrl-wrapper c-ctrl-wrapper--menus-left">
|
||||
<button
|
||||
class="c-button--menu icon-notebook"
|
||||
class="c-icon-button c-button--menu icon-camera"
|
||||
title="Take a Notebook Snapshot"
|
||||
@click="setNotebookTypes"
|
||||
@click.stop="toggleMenu"
|
||||
@click.stop.prevent="showMenu"
|
||||
>
|
||||
<span class="c-button__label"></span>
|
||||
<span
|
||||
title="Take Notebook Snapshot"
|
||||
class="c-icon-button__label"
|
||||
>
|
||||
Snapshot
|
||||
</span>
|
||||
</button>
|
||||
<div
|
||||
v-show="showMenu"
|
||||
class="c-menu"
|
||||
>
|
||||
<ul>
|
||||
<li
|
||||
v-for="(type, index) in notebookTypes"
|
||||
:key="index"
|
||||
:class="type.cssClass"
|
||||
:title="type.name"
|
||||
@click="snapshot(type)"
|
||||
>
|
||||
{{ type.name }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -57,22 +45,20 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
notebookSnapshot: null,
|
||||
notebookTypes: [],
|
||||
showMenu: false
|
||||
notebookTypes: []
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.notebookSnapshot = new Snapshot(this.openmct);
|
||||
|
||||
document.addEventListener('click', this.hideMenu);
|
||||
},
|
||||
destroyed() {
|
||||
document.removeEventListener('click', this.hideMenu);
|
||||
this.setDefaultNotebookStatus();
|
||||
},
|
||||
methods: {
|
||||
setNotebookTypes() {
|
||||
showMenu(event) {
|
||||
const notebookTypes = [];
|
||||
const defaultNotebook = getDefaultNotebook();
|
||||
const elementBoundingClientRect = this.$el.getBoundingClientRect();
|
||||
const x = elementBoundingClientRect.x;
|
||||
const y = elementBoundingClientRect.y + elementBoundingClientRect.height;
|
||||
|
||||
if (defaultNotebook) {
|
||||
const domainObject = defaultNotebook.domainObject;
|
||||
@ -83,35 +69,31 @@ export default {
|
||||
notebookTypes.push({
|
||||
cssClass: 'icon-notebook',
|
||||
name: `Save to Notebook ${defaultPath}`,
|
||||
type: NOTEBOOK_DEFAULT
|
||||
callBack: () => {
|
||||
return this.snapshot(NOTEBOOK_DEFAULT);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
notebookTypes.push({
|
||||
cssClass: 'icon-notebook',
|
||||
cssClass: 'icon-camera',
|
||||
name: 'Save to Notebook Snapshots',
|
||||
type: NOTEBOOK_SNAPSHOT
|
||||
callBack: () => {
|
||||
return this.snapshot(NOTEBOOK_SNAPSHOT);
|
||||
}
|
||||
});
|
||||
|
||||
this.notebookTypes = notebookTypes;
|
||||
},
|
||||
toggleMenu() {
|
||||
this.showMenu = !this.showMenu;
|
||||
},
|
||||
hideMenu() {
|
||||
this.showMenu = false;
|
||||
this.openmct.menus.showMenu(x, y, notebookTypes);
|
||||
},
|
||||
snapshot(notebook) {
|
||||
this.hideMenu();
|
||||
|
||||
this.$nextTick(() => {
|
||||
const element = document.querySelector('.c-overlay__contents')
|
||||
|| document.getElementsByClassName('l-shell__main-container')[0];
|
||||
|
||||
const bounds = this.openmct.time.bounds();
|
||||
const link = !this.ignoreLink
|
||||
? window.location.href
|
||||
? window.location.hash
|
||||
: null;
|
||||
|
||||
const objectPath = this.objectPath || this.openmct.router.path;
|
||||
@ -124,6 +106,15 @@ export default {
|
||||
|
||||
this.notebookSnapshot.capture(snapshotMeta, notebook.type, element);
|
||||
});
|
||||
},
|
||||
setDefaultNotebookStatus() {
|
||||
let defaultNotebookObject = getDefaultNotebook();
|
||||
|
||||
if (defaultNotebookObject && defaultNotebookObject.notebookMeta) {
|
||||
let notebookIdentifier = defaultNotebookObject.notebookMeta.identifier;
|
||||
|
||||
this.openmct.status.set(notebookIdentifier, 'notebook-default');
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
@ -4,7 +4,7 @@
|
||||
<div class="l-browse-bar__start">
|
||||
<div class="l-browse-bar__object-name--w">
|
||||
<div class="l-browse-bar__object-name c-object-label">
|
||||
<div class="c-object-label__type-icon icon-notebook"></div>
|
||||
<div class="c-object-label__type-icon icon-camera"></div>
|
||||
<div class="c-object-label__name">
|
||||
Notebook Snapshots
|
||||
<span v-if="snapshots.length"
|
||||
@ -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);
|
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="c-indicator c-indicator--clickable icon-notebook"
|
||||
<div class="c-indicator c-indicator--clickable icon-camera"
|
||||
:class="[
|
||||
{ 's-status-off': snapshotCount === 0 },
|
||||
{ 's-status-on': snapshotCount > 0 },
|
||||
@ -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,6 @@
|
||||
import Notebook from './components/notebook.vue';
|
||||
import NotebookSnapshotIndicator from './components/notebook-snapshot-indicator.vue';
|
||||
import CopyToNotebookAction from './actions/CopyToNotebookAction';
|
||||
import Notebook from './components/Notebook.vue';
|
||||
import NotebookSnapshotIndicator from './components/NotebookSnapshotIndicator.vue';
|
||||
import SnapshotContainer from './snapshot-container';
|
||||
import Vue from 'vue';
|
||||
|
||||
@ -13,6 +14,8 @@ export default function NotebookPlugin() {
|
||||
|
||||
installed = true;
|
||||
|
||||
openmct.actions.register(new CopyToNotebookAction(openmct));
|
||||
|
||||
const notebookType = {
|
||||
name: 'Notebook',
|
||||
description: 'Create and save timestamped notes with embedded object snapshots.',
|
||||
@ -95,7 +98,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);
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user