mirror of
https://github.com/nasa/openmct.git
synced 2025-06-25 18:50:11 +00:00
Compare commits
125 Commits
snapshot-f
...
couchdb-ob
Author | SHA1 | Date | |
---|---|---|---|
4f3317969e | |||
56780879b0 | |||
2093a9d687 | |||
ea3b7d3da4 | |||
3932ccdbad | |||
dd45fa3a7c | |||
dd3d4c8c3a | |||
4047c888be | |||
6f3004898b | |||
44f05d16c5 | |||
813e4a5656 | |||
1499286bee | |||
ea66292141 | |||
8fdb983a3d | |||
8e04d6409f | |||
91ce09217a | |||
6226763c37 | |||
7623a0648f | |||
b7085f7f62 | |||
ca6e9387c3 | |||
5734a1a69f | |||
55c851873c | |||
2b143dfc0f | |||
9405272f3b | |||
ab3319128d | |||
a9be9f1827 | |||
abb1a5c75b | |||
46d00e6d61 | |||
56cd0cb5e1 | |||
5e2fe7dc42 | |||
de303d6497 | |||
64c9d29059 | |||
4794cd5711 | |||
e4d6e90c35 | |||
84d9a525a9 | |||
0aca0ce6a6 | |||
c0742d521c | |||
5b3762e90f | |||
6eadddd8d2 | |||
92737b43af | |||
8b0f6885ee | |||
9af2d15cef | |||
e60d8d08a4 | |||
3e9b567fce | |||
6f51de85db | |||
f202ae19cb | |||
668bd75025 | |||
e6e07cf959 | |||
2f8431905f | |||
23aba14dfe | |||
b0fa955914 | |||
98207a3e0d | |||
26b81345f2 | |||
ac2034b243 | |||
351848ad56 | |||
cbac495f93 | |||
15ef5b7623 | |||
46c7ac944f | |||
aa4bfab462 | |||
f5cbb37e5a | |||
8d9079984a | |||
41783d8939 | |||
441ad58fe7 | |||
06a6a3f773 | |||
52fab78625 | |||
5eb6c15959 | |||
ce8c31cfa4 | |||
d80c0eef8e | |||
55829dcf05 | |||
d78956327c | |||
4a0728a55b | |||
2e1d57aa8c | |||
1c2b0678be | |||
6f810add43 | |||
12727adb16 | |||
9da750c3bb | |||
176226ddef | |||
acea18fa70 | |||
d1656f8561 | |||
87751e882c | |||
dff393a714 | |||
fd9c9aee03 | |||
59bf981fb0 | |||
4bbdac759f | |||
13fe7509de | |||
6fd8f6cd43 | |||
6375ecda34 | |||
d232dacc65 | |||
59946e89ef | |||
d75c4b4049 | |||
30ca4b707d | |||
27704c9a48 | |||
b0203f2272 | |||
77b720d00d | |||
ba982671b2 | |||
5df7d92d64 | |||
a8228406de | |||
2401473012 | |||
e502fb88fa | |||
37a52cb011 | |||
04fb4e8a82 | |||
5646a252f7 | |||
0e6ce7f58b | |||
8cd6a4c6a3 | |||
02fc162197 | |||
84d21a3695 | |||
1a6369c2b9 | |||
463c44679d | |||
c1f3ea4e61 | |||
142b767470 | |||
184b716b53 | |||
e53399495b | |||
d27f73579b | |||
1ae8199e89 | |||
2deb4e8474 | |||
7f10681424 | |||
c756adad6f | |||
f3d593bc1e | |||
b637307de6 | |||
b6e0208e71 | |||
631876cab3 | |||
a192d46c2b | |||
6923f17645 | |||
87a45de05b | |||
ab76451360 |
20
.eslintrc.js
20
.eslintrc.js
@ -54,7 +54,7 @@ module.exports = {
|
|||||||
{
|
{
|
||||||
"anonymous": "always",
|
"anonymous": "always",
|
||||||
"asyncArrow": "always",
|
"asyncArrow": "always",
|
||||||
"named": "never",
|
"named": "never"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"array-bracket-spacing": "error",
|
"array-bracket-spacing": "error",
|
||||||
@ -178,7 +178,10 @@ module.exports = {
|
|||||||
//https://eslint.org/docs/rules/no-whitespace-before-property
|
//https://eslint.org/docs/rules/no-whitespace-before-property
|
||||||
"no-whitespace-before-property": "error",
|
"no-whitespace-before-property": "error",
|
||||||
// https://eslint.org/docs/rules/object-curly-newline
|
// https://eslint.org/docs/rules/object-curly-newline
|
||||||
"object-curly-newline": ["error", {"consistent": true, "multiline": true}],
|
"object-curly-newline": ["error", {
|
||||||
|
"consistent": true,
|
||||||
|
"multiline": true
|
||||||
|
}],
|
||||||
// https://eslint.org/docs/rules/object-property-newline
|
// https://eslint.org/docs/rules/object-property-newline
|
||||||
"object-property-newline": "error",
|
"object-property-newline": "error",
|
||||||
// https://eslint.org/docs/rules/brace-style
|
// https://eslint.org/docs/rules/brace-style
|
||||||
@ -188,7 +191,7 @@ module.exports = {
|
|||||||
// https://eslint.org/docs/rules/operator-linebreak
|
// https://eslint.org/docs/rules/operator-linebreak
|
||||||
"operator-linebreak": ["error", "before", {"overrides": {"=": "after"}}],
|
"operator-linebreak": ["error", "before", {"overrides": {"=": "after"}}],
|
||||||
// https://eslint.org/docs/rules/padding-line-between-statements
|
// https://eslint.org/docs/rules/padding-line-between-statements
|
||||||
"padding-line-between-statements":["error", {
|
"padding-line-between-statements": ["error", {
|
||||||
"blankLine": "always",
|
"blankLine": "always",
|
||||||
"prev": "multiline-block-like",
|
"prev": "multiline-block-like",
|
||||||
"next": "*"
|
"next": "*"
|
||||||
@ -200,11 +203,17 @@ module.exports = {
|
|||||||
// https://eslint.org/docs/rules/space-infix-ops
|
// https://eslint.org/docs/rules/space-infix-ops
|
||||||
"space-infix-ops": "error",
|
"space-infix-ops": "error",
|
||||||
// https://eslint.org/docs/rules/space-unary-ops
|
// https://eslint.org/docs/rules/space-unary-ops
|
||||||
"space-unary-ops": ["error", {"words": true, "nonwords": false}],
|
"space-unary-ops": ["error", {
|
||||||
|
"words": true,
|
||||||
|
"nonwords": false
|
||||||
|
}],
|
||||||
// https://eslint.org/docs/rules/arrow-spacing
|
// https://eslint.org/docs/rules/arrow-spacing
|
||||||
"arrow-spacing": "error",
|
"arrow-spacing": "error",
|
||||||
// https://eslint.org/docs/rules/semi-spacing
|
// https://eslint.org/docs/rules/semi-spacing
|
||||||
"semi-spacing": ["error", {"before": false, "after": true}],
|
"semi-spacing": ["error", {
|
||||||
|
"before": false,
|
||||||
|
"after": true
|
||||||
|
}],
|
||||||
|
|
||||||
"vue/html-indent": [
|
"vue/html-indent": [
|
||||||
"error",
|
"error",
|
||||||
@ -237,6 +246,7 @@ module.exports = {
|
|||||||
}],
|
}],
|
||||||
"vue/multiline-html-element-content-newline": "off",
|
"vue/multiline-html-element-content-newline": "off",
|
||||||
"vue/singleline-html-element-content-newline": "off",
|
"vue/singleline-html-element-content-newline": "off",
|
||||||
|
"vue/no-mutating-props": "off"
|
||||||
|
|
||||||
},
|
},
|
||||||
"overrides": [
|
"overrides": [
|
||||||
|
@ -182,7 +182,7 @@ The following guidelines are provided for anyone contributing source code to the
|
|||||||
1. Avoid the use of "magic" values.
|
1. Avoid the use of "magic" values.
|
||||||
eg.
|
eg.
|
||||||
```JavaScript
|
```JavaScript
|
||||||
Const UNAUTHORIZED = 401
|
const UNAUTHORIZED = 401;
|
||||||
if (responseCode === UNAUTHORIZED)
|
if (responseCode === UNAUTHORIZED)
|
||||||
```
|
```
|
||||||
is preferable to
|
is preferable to
|
||||||
|
@ -76,6 +76,7 @@ define([
|
|||||||
|
|
||||||
workerRequest[prop] = Number(workerRequest[prop]);
|
workerRequest[prop] = Number(workerRequest[prop]);
|
||||||
});
|
});
|
||||||
|
|
||||||
workerRequest.name = domainObject.name;
|
workerRequest.name = domainObject.name;
|
||||||
|
|
||||||
return workerRequest;
|
return workerRequest;
|
||||||
|
@ -108,7 +108,6 @@
|
|||||||
|
|
||||||
for (; nextStep < end && data.length < 5000; nextStep += step) {
|
for (; nextStep < end && data.length < 5000; nextStep += step) {
|
||||||
data.push({
|
data.push({
|
||||||
name: request.name,
|
|
||||||
utc: nextStep,
|
utc: nextStep,
|
||||||
yesterday: nextStep - 60 * 60 * 24 * 1000,
|
yesterday: nextStep - 60 * 60 * 24 * 1000,
|
||||||
sin: sin(nextStep, period, amplitude, offset, phase, randomness),
|
sin: sin(nextStep, period, amplitude, offset, phase, randomness),
|
||||||
|
@ -27,7 +27,7 @@ define([
|
|||||||
) {
|
) {
|
||||||
function ImageryPlugin() {
|
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-18731.jpg",
|
||||||
"https://www.hq.nasa.gov/alsj/a16/AS16-117-18732.jpg",
|
"https://www.hq.nasa.gov/alsj/a16/AS16-117-18732.jpg",
|
||||||
"https://www.hq.nasa.gov/alsj/a16/AS16-117-18733.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-18747.jpg",
|
||||||
"https://www.hq.nasa.gov/alsj/a16/AS16-117-18748.jpg"
|
"https://www.hq.nasa.gov/alsj/a16/AS16-117-18748.jpg"
|
||||||
];
|
];
|
||||||
|
const IMAGE_DELAY = 20000;
|
||||||
|
|
||||||
function pointForTimestamp(timestamp, name) {
|
function pointForTimestamp(timestamp, name) {
|
||||||
return {
|
return {
|
||||||
name: name,
|
name: name,
|
||||||
utc: Math.floor(timestamp / 5000) * 5000,
|
utc: Math.floor(timestamp / IMAGE_DELAY) * IMAGE_DELAY,
|
||||||
local: Math.floor(timestamp / 5000) * 5000,
|
local: Math.floor(timestamp / IMAGE_DELAY) * IMAGE_DELAY,
|
||||||
url: IMAGE_SAMPLES[Math.floor(timestamp / 5000) % IMAGE_SAMPLES.length]
|
url: IMAGE_SAMPLES[Math.floor(timestamp / IMAGE_DELAY) % IMAGE_SAMPLES.length]
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -64,7 +65,7 @@ define([
|
|||||||
subscribe: function (domainObject, callback) {
|
subscribe: function (domainObject, callback) {
|
||||||
var interval = setInterval(function () {
|
var interval = setInterval(function () {
|
||||||
callback(pointForTimestamp(Date.now(), domainObject.name));
|
callback(pointForTimestamp(Date.now(), domainObject.name));
|
||||||
}, 5000);
|
}, IMAGE_DELAY);
|
||||||
|
|
||||||
return function () {
|
return function () {
|
||||||
clearInterval(interval);
|
clearInterval(interval);
|
||||||
@ -81,9 +82,9 @@ define([
|
|||||||
var start = options.start;
|
var start = options.start;
|
||||||
var end = Math.min(options.end, Date.now());
|
var end = Math.min(options.end, Date.now());
|
||||||
var data = [];
|
var data = [];
|
||||||
while (start <= end && data.length < 5000) {
|
while (start <= end && data.length < IMAGE_DELAY) {
|
||||||
data.push(pointForTimestamp(start, domainObject.name));
|
data.push(pointForTimestamp(start, domainObject.name));
|
||||||
start += 5000;
|
start += IMAGE_DELAY;
|
||||||
}
|
}
|
||||||
|
|
||||||
return Promise.resolve(data);
|
return Promise.resolve(data);
|
||||||
|
@ -138,7 +138,7 @@ define([
|
|||||||
"id": "styleguide:home",
|
"id": "styleguide:home",
|
||||||
"priority": "preferred",
|
"priority": "preferred",
|
||||||
"model": {
|
"model": {
|
||||||
"type": "folder",
|
"type": "noneditable.folder",
|
||||||
"name": "Style Guide Home",
|
"name": "Style Guide Home",
|
||||||
"location": "ROOT",
|
"location": "ROOT",
|
||||||
"composition": [
|
"composition": [
|
||||||
@ -155,7 +155,7 @@ define([
|
|||||||
"id": "styleguide:ui-elements",
|
"id": "styleguide:ui-elements",
|
||||||
"priority": "preferred",
|
"priority": "preferred",
|
||||||
"model": {
|
"model": {
|
||||||
"type": "folder",
|
"type": "noneditable.folder",
|
||||||
"name": "UI Elements",
|
"name": "UI Elements",
|
||||||
"location": "styleguide:home",
|
"location": "styleguide:home",
|
||||||
"composition": [
|
"composition": [
|
||||||
|
89
index.html
89
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-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-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">
|
<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>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
</body>
|
</body>
|
||||||
<script>
|
<script>
|
||||||
const THIRTY_SECONDS = 30 * 1000;
|
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'
|
'example/eventGenerator'
|
||||||
@ -73,30 +111,30 @@
|
|||||||
{
|
{
|
||||||
label: 'Last Day',
|
label: 'Last Day',
|
||||||
bounds: {
|
bounds: {
|
||||||
start: () => Date.now() - 1000 * 60 * 60 * 24,
|
start: () => Date.now() - ONE_DAY,
|
||||||
end: () => Date.now()
|
end: () => Date.now()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Last 2 hours',
|
label: 'Last 2 hours',
|
||||||
bounds: {
|
bounds: {
|
||||||
start: () => Date.now() - 1000 * 60 * 60 * 2,
|
start: () => Date.now() - TWO_HOURS,
|
||||||
end: () => Date.now()
|
end: () => Date.now()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Last hour',
|
label: 'Last hour',
|
||||||
bounds: {
|
bounds: {
|
||||||
start: () => Date.now() - 1000 * 60 * 60,
|
start: () => Date.now() - ONE_HOUR,
|
||||||
end: () => Date.now()
|
end: () => Date.now()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
// maximum recent bounds to retain in conductor history
|
// maximum recent bounds to retain in conductor history
|
||||||
records: 10,
|
records: 10
|
||||||
// maximum duration between start and end bounds
|
// maximum duration between start and end bounds
|
||||||
// for utc-based time systems this is in milliseconds
|
// for utc-based time systems this is in milliseconds
|
||||||
limit: 1000 * 60 * 60 * 24
|
// limit: ONE_DAY
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Realtime",
|
name: "Realtime",
|
||||||
@ -105,7 +143,44 @@
|
|||||||
clockOffsets: {
|
clockOffsets: {
|
||||||
start: - THIRTY_MINUTES,
|
start: - THIRTY_MINUTES,
|
||||||
end: THIRTY_SECONDS
|
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'],
|
reports: ['html', 'lcovonly', 'text-summary'],
|
||||||
thresholds: {
|
thresholds: {
|
||||||
global: {
|
global: {
|
||||||
lines: 65
|
lines: 66
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "openmct",
|
"name": "openmct",
|
||||||
"version": "1.3.0-SNAPSHOT",
|
"version": "1.6.2-SNAPSHOT",
|
||||||
"description": "The Open MCT core platform",
|
"description": "The Open MCT core platform",
|
||||||
"dependencies": {},
|
"dependencies": {},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@ -23,7 +23,7 @@
|
|||||||
"d3-time": "1.0.x",
|
"d3-time": "1.0.x",
|
||||||
"d3-time-format": "2.1.x",
|
"d3-time-format": "2.1.x",
|
||||||
"eslint": "7.0.0",
|
"eslint": "7.0.0",
|
||||||
"eslint-plugin-vue": "^6.0.0",
|
"eslint-plugin-vue": "^7.5.0",
|
||||||
"eslint-plugin-you-dont-need-lodash-underscore": "^6.10.0",
|
"eslint-plugin-you-dont-need-lodash-underscore": "^6.10.0",
|
||||||
"eventemitter3": "^1.2.0",
|
"eventemitter3": "^1.2.0",
|
||||||
"exports-loader": "^0.7.0",
|
"exports-loader": "^0.7.0",
|
||||||
|
@ -143,8 +143,8 @@ define([
|
|||||||
"$window"
|
"$window"
|
||||||
],
|
],
|
||||||
"group": "windowing",
|
"group": "windowing",
|
||||||
"cssClass": "icon-new-window",
|
"priority": 10,
|
||||||
"priority": "preferred"
|
"cssClass": "icon-new-window"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"runs": [
|
"runs": [
|
||||||
|
@ -139,7 +139,9 @@ define([
|
|||||||
],
|
],
|
||||||
"description": "Edit",
|
"description": "Edit",
|
||||||
"category": "view-control",
|
"category": "view-control",
|
||||||
"cssClass": "major icon-pencil"
|
"cssClass": "major icon-pencil",
|
||||||
|
"group": "action",
|
||||||
|
"priority": 10
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "properties",
|
"key": "properties",
|
||||||
@ -150,6 +152,8 @@ define([
|
|||||||
"implementation": PropertiesAction,
|
"implementation": PropertiesAction,
|
||||||
"cssClass": "major icon-pencil",
|
"cssClass": "major icon-pencil",
|
||||||
"name": "Edit Properties...",
|
"name": "Edit Properties...",
|
||||||
|
"group": "action",
|
||||||
|
"priority": 10,
|
||||||
"description": "Edit properties of this object.",
|
"description": "Edit properties of this object.",
|
||||||
"depends": [
|
"depends": [
|
||||||
"dialogService"
|
"dialogService"
|
||||||
|
@ -44,9 +44,9 @@ define(
|
|||||||
// is also invoked during the create process which should be allowed,
|
// is also invoked during the create process which should be allowed,
|
||||||
// because it may be saved elsewhere
|
// because it may be saved elsewhere
|
||||||
if ((key === 'edit' && category === 'view-control') || key === 'properties') {
|
if ((key === 'edit' && category === 'view-control') || key === 'properties') {
|
||||||
let newStyleObject = objectUtils.toNewFormat(domainObject, domainObject.getId());
|
let identifier = this.openmct.objects.parseKeyString(domainObject.getId());
|
||||||
|
|
||||||
return this.openmct.objects.isPersistable(newStyleObject);
|
return this.openmct.objects.isPersistable(identifier);
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
@ -43,7 +43,8 @@ define(
|
|||||||
);
|
);
|
||||||
|
|
||||||
mockObjectAPI = jasmine.createSpyObj('objectAPI', [
|
mockObjectAPI = jasmine.createSpyObj('objectAPI', [
|
||||||
'isPersistable'
|
'isPersistable',
|
||||||
|
'parseKeyString'
|
||||||
]);
|
]);
|
||||||
|
|
||||||
mockAPI = {
|
mockAPI = {
|
||||||
|
@ -20,12 +20,12 @@
|
|||||||
at runtime from the About dialog for additional information.
|
at runtime from the About dialog for additional information.
|
||||||
-->
|
-->
|
||||||
<div class="c-object-label"
|
<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()}}"
|
<div class="c-object-label__type-icon {{type.getCssClass()}}"
|
||||||
ng-class="{ 'l-icon-link':location.isLink() }"
|
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>
|
||||||
<div class='c-object-label__name'>{{model.name}}</div>
|
<div class='c-object-label__name'>{{model.name}}</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -48,9 +48,9 @@ define(
|
|||||||
// prevents editing of objects that cannot be persisted, so we can assume that this
|
// prevents editing of objects that cannot be persisted, so we can assume that this
|
||||||
// is a new object.
|
// is a new object.
|
||||||
if (!(parent.hasCapability('editor') && parent.getCapability('editor').isEditContextRoot())) {
|
if (!(parent.hasCapability('editor') && parent.getCapability('editor').isEditContextRoot())) {
|
||||||
let newStyleObject = objectUtils.toNewFormat(parent, parent.getId());
|
let identifier = this.openmct.objects.parseKeyString(parent.getId());
|
||||||
|
|
||||||
return this.openmct.objects.isPersistable(newStyleObject);
|
return this.openmct.objects.isPersistable(identifier);
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
@ -33,7 +33,8 @@ define(
|
|||||||
|
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
objectAPI = jasmine.createSpyObj('objectsAPI', [
|
objectAPI = jasmine.createSpyObj('objectsAPI', [
|
||||||
'isPersistable'
|
'isPersistable',
|
||||||
|
'parseKeyString'
|
||||||
]);
|
]);
|
||||||
|
|
||||||
mockOpenMCT = {
|
mockOpenMCT = {
|
||||||
|
@ -114,7 +114,12 @@ define(["objectUtils"],
|
|||||||
var self = this,
|
var self = this,
|
||||||
domainObject = this.domainObject;
|
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
|
return this.openmct.objects
|
||||||
.save(newStyleObject)
|
.save(newStyleObject)
|
||||||
@ -146,6 +151,7 @@ define(["objectUtils"],
|
|||||||
return domainObject.useCapability("mutation", function () {
|
return domainObject.useCapability("mutation", function () {
|
||||||
return model;
|
return model;
|
||||||
}, modified);
|
}, modified);
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -99,8 +99,8 @@ define(
|
|||||||
|
|
||||||
mockNewStyleDomainObject = Object.assign({}, model);
|
mockNewStyleDomainObject = Object.assign({}, model);
|
||||||
mockNewStyleDomainObject.identifier = {
|
mockNewStyleDomainObject.identifier = {
|
||||||
namespace: "",
|
namespace: SPACE,
|
||||||
key: id
|
key: key
|
||||||
};
|
};
|
||||||
|
|
||||||
// Simulate mutation capability
|
// Simulate mutation capability
|
||||||
|
@ -21,32 +21,24 @@
|
|||||||
*****************************************************************************/
|
*****************************************************************************/
|
||||||
|
|
||||||
define([
|
define([
|
||||||
"./src/actions/MoveAction",
|
|
||||||
"./src/actions/CopyAction",
|
|
||||||
"./src/actions/LinkAction",
|
"./src/actions/LinkAction",
|
||||||
"./src/actions/SetPrimaryLocationAction",
|
"./src/actions/SetPrimaryLocationAction",
|
||||||
"./src/services/LocatingCreationDecorator",
|
"./src/services/LocatingCreationDecorator",
|
||||||
"./src/services/LocatingObjectDecorator",
|
"./src/services/LocatingObjectDecorator",
|
||||||
"./src/policies/CopyPolicy",
|
"./src/policies/CopyPolicy",
|
||||||
"./src/policies/CrossSpacePolicy",
|
"./src/policies/CrossSpacePolicy",
|
||||||
"./src/policies/MovePolicy",
|
|
||||||
"./src/capabilities/LocationCapability",
|
"./src/capabilities/LocationCapability",
|
||||||
"./src/services/MoveService",
|
|
||||||
"./src/services/LinkService",
|
"./src/services/LinkService",
|
||||||
"./src/services/CopyService",
|
"./src/services/CopyService",
|
||||||
"./src/services/LocationService"
|
"./src/services/LocationService"
|
||||||
], function (
|
], function (
|
||||||
MoveAction,
|
|
||||||
CopyAction,
|
|
||||||
LinkAction,
|
LinkAction,
|
||||||
SetPrimaryLocationAction,
|
SetPrimaryLocationAction,
|
||||||
LocatingCreationDecorator,
|
LocatingCreationDecorator,
|
||||||
LocatingObjectDecorator,
|
LocatingObjectDecorator,
|
||||||
CopyPolicy,
|
CopyPolicy,
|
||||||
CrossSpacePolicy,
|
CrossSpacePolicy,
|
||||||
MovePolicy,
|
|
||||||
LocationCapability,
|
LocationCapability,
|
||||||
MoveService,
|
|
||||||
LinkService,
|
LinkService,
|
||||||
CopyService,
|
CopyService,
|
||||||
LocationService
|
LocationService
|
||||||
@ -60,41 +52,14 @@ define([
|
|||||||
"configuration": {},
|
"configuration": {},
|
||||||
"extensions": {
|
"extensions": {
|
||||||
"actions": [
|
"actions": [
|
||||||
{
|
|
||||||
"key": "move",
|
|
||||||
"name": "Move",
|
|
||||||
"description": "Move object to another location.",
|
|
||||||
"cssClass": "icon-move",
|
|
||||||
"category": "contextual",
|
|
||||||
"implementation": MoveAction,
|
|
||||||
"depends": [
|
|
||||||
"policyService",
|
|
||||||
"locationService",
|
|
||||||
"moveService"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "copy",
|
|
||||||
"name": "Duplicate",
|
|
||||||
"description": "Duplicate object to another location.",
|
|
||||||
"cssClass": "icon-duplicate",
|
|
||||||
"category": "contextual",
|
|
||||||
"implementation": CopyAction,
|
|
||||||
"depends": [
|
|
||||||
"$log",
|
|
||||||
"policyService",
|
|
||||||
"locationService",
|
|
||||||
"copyService",
|
|
||||||
"dialogService",
|
|
||||||
"notificationService"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"key": "link",
|
"key": "link",
|
||||||
"name": "Create Link",
|
"name": "Create Link",
|
||||||
"description": "Create Link to object in another location.",
|
"description": "Create Link to object in another location.",
|
||||||
"cssClass": "icon-link",
|
"cssClass": "icon-link",
|
||||||
"category": "contextual",
|
"category": "contextual",
|
||||||
|
"group": "action",
|
||||||
|
"priority": 7,
|
||||||
"implementation": LinkAction,
|
"implementation": LinkAction,
|
||||||
"depends": [
|
"depends": [
|
||||||
"policyService",
|
"policyService",
|
||||||
@ -135,10 +100,6 @@ define([
|
|||||||
{
|
{
|
||||||
"category": "action",
|
"category": "action",
|
||||||
"implementation": CopyPolicy
|
"implementation": CopyPolicy
|
||||||
},
|
|
||||||
{
|
|
||||||
"category": "action",
|
|
||||||
"implementation": MovePolicy
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"capabilities": [
|
"capabilities": [
|
||||||
@ -154,17 +115,6 @@ define([
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"services": [
|
"services": [
|
||||||
{
|
|
||||||
"key": "moveService",
|
|
||||||
"name": "Move Service",
|
|
||||||
"description": "Provides a service for moving objects",
|
|
||||||
"implementation": MoveService,
|
|
||||||
"depends": [
|
|
||||||
"openmct",
|
|
||||||
"linkService",
|
|
||||||
"$q"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"key": "linkService",
|
"key": "linkService",
|
||||||
"name": "Link Service",
|
"name": "Link Service",
|
||||||
|
@ -1,168 +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(
|
|
||||||
['./AbstractComposeAction', './CancelError'],
|
|
||||||
function (AbstractComposeAction, CancelError) {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The CopyAction is available from context menus and allows a user to
|
|
||||||
* deep copy an object to another location of their choosing.
|
|
||||||
*
|
|
||||||
* @implements {Action}
|
|
||||||
* @constructor
|
|
||||||
* @memberof platform/entanglement
|
|
||||||
*/
|
|
||||||
function CopyAction(
|
|
||||||
$log,
|
|
||||||
policyService,
|
|
||||||
locationService,
|
|
||||||
copyService,
|
|
||||||
dialogService,
|
|
||||||
notificationService,
|
|
||||||
context
|
|
||||||
) {
|
|
||||||
this.dialog = undefined;
|
|
||||||
this.notification = undefined;
|
|
||||||
this.dialogService = dialogService;
|
|
||||||
this.notificationService = notificationService;
|
|
||||||
this.$log = $log;
|
|
||||||
//Extend the behaviour of the Abstract Compose Action
|
|
||||||
AbstractComposeAction.call(
|
|
||||||
this,
|
|
||||||
policyService,
|
|
||||||
locationService,
|
|
||||||
copyService,
|
|
||||||
context,
|
|
||||||
"Duplicate",
|
|
||||||
"To a Location"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
CopyAction.prototype = Object.create(AbstractComposeAction.prototype);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates user about progress of copy. Should not be invoked by
|
|
||||||
* client code under any circumstances.
|
|
||||||
*
|
|
||||||
* @private
|
|
||||||
* @param phase
|
|
||||||
* @param totalObjects
|
|
||||||
* @param processed
|
|
||||||
*/
|
|
||||||
CopyAction.prototype.progress = function (phase, totalObjects, processed) {
|
|
||||||
/*
|
|
||||||
Copy has two distinct phases. In the first phase a copy plan is
|
|
||||||
made in memory. During this phase of execution, the user is
|
|
||||||
shown a blocking 'modal' dialog.
|
|
||||||
|
|
||||||
In the second phase, the copying is taking place, and the user
|
|
||||||
is shown non-invasive banner notifications at the bottom of the screen.
|
|
||||||
*/
|
|
||||||
if (phase.toLowerCase() === 'preparing' && !this.dialog) {
|
|
||||||
this.dialog = this.dialogService.showBlockingMessage({
|
|
||||||
title: "Preparing to copy objects",
|
|
||||||
hint: "Do not navigate away from this page or close this browser tab while this message is displayed.",
|
|
||||||
unknownProgress: true,
|
|
||||||
severity: "info"
|
|
||||||
});
|
|
||||||
} else if (phase.toLowerCase() === "copying") {
|
|
||||||
if (this.dialog) {
|
|
||||||
this.dialog.dismiss();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.notification) {
|
|
||||||
this.notification = this.notificationService
|
|
||||||
.notify({
|
|
||||||
title: "Copying objects",
|
|
||||||
unknownProgress: false,
|
|
||||||
severity: "info"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
this.notification.model.progress = (processed / totalObjects) * 100;
|
|
||||||
this.notification.model.title = ["Copied ", processed, "of ",
|
|
||||||
totalObjects, "objects"].join(" ");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Executes the CopyAction. The CopyAction uses the default behaviour of
|
|
||||||
* the AbstractComposeAction, but extends it to support notification
|
|
||||||
* updates of progress on copy.
|
|
||||||
*/
|
|
||||||
CopyAction.prototype.perform = function () {
|
|
||||||
var self = this;
|
|
||||||
|
|
||||||
function success(domainObject) {
|
|
||||||
var domainObjectName = domainObject.model.name;
|
|
||||||
|
|
||||||
self.notification.dismiss();
|
|
||||||
self.notificationService.info(domainObjectName + " copied successfully.");
|
|
||||||
}
|
|
||||||
|
|
||||||
function error(errorDetails) {
|
|
||||||
// No need to notify user of their own cancellation
|
|
||||||
if (errorDetails instanceof CancelError) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var errorDialog,
|
|
||||||
errorMessage = {
|
|
||||||
title: "Error copying objects.",
|
|
||||||
severity: "error",
|
|
||||||
hint: errorDetails.message,
|
|
||||||
minimized: true, // want the notification to be minimized initially (don't show banner)
|
|
||||||
options: [{
|
|
||||||
label: "OK",
|
|
||||||
callback: function () {
|
|
||||||
errorDialog.dismiss();
|
|
||||||
}
|
|
||||||
}]
|
|
||||||
};
|
|
||||||
|
|
||||||
self.dialog.dismiss();
|
|
||||||
if (self.notification) {
|
|
||||||
self.notification.dismiss(); // Clear the progress notification
|
|
||||||
}
|
|
||||||
|
|
||||||
self.$log.error("Error copying objects. ", errorDetails);
|
|
||||||
//Show a minimized notification of error for posterity
|
|
||||||
self.notificationService.notify(errorMessage);
|
|
||||||
//Display a blocking message
|
|
||||||
errorDialog = self.dialogService.showBlockingMessage(errorMessage);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
function notification(details) {
|
|
||||||
self.progress(details.phase, details.totalObjects, details.processed);
|
|
||||||
}
|
|
||||||
|
|
||||||
return AbstractComposeAction.prototype.perform.call(this)
|
|
||||||
.then(success, error, notification);
|
|
||||||
};
|
|
||||||
|
|
||||||
CopyAction.appliesTo = AbstractComposeAction.appliesTo;
|
|
||||||
|
|
||||||
return CopyAction;
|
|
||||||
}
|
|
||||||
);
|
|
@ -1,104 +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(
|
|
||||||
function () {
|
|
||||||
/**
|
|
||||||
* MoveService provides an interface for moving objects from one
|
|
||||||
* location to another. It also provides a method for determining if
|
|
||||||
* an object can be copied to a specific location.
|
|
||||||
* @constructor
|
|
||||||
* @memberof platform/entanglement
|
|
||||||
* @implements {platform/entanglement.AbstractComposeService}
|
|
||||||
*/
|
|
||||||
function MoveService(openmct, linkService) {
|
|
||||||
this.openmct = openmct;
|
|
||||||
this.linkService = linkService;
|
|
||||||
}
|
|
||||||
|
|
||||||
MoveService.prototype.validate = function (object, parentCandidate) {
|
|
||||||
var currentParent = object
|
|
||||||
.getCapability('context')
|
|
||||||
.getParent();
|
|
||||||
|
|
||||||
if (!parentCandidate || !parentCandidate.getId) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (parentCandidate.getId() === currentParent.getId()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (parentCandidate.getId() === object.getId()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (parentCandidate.getModel().composition.indexOf(object.getId()) !== -1) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.openmct.composition.checkPolicy(
|
|
||||||
parentCandidate.useCapability('adapter'),
|
|
||||||
object.useCapability('adapter')
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
MoveService.prototype.perform = function (object, parentObject) {
|
|
||||||
function relocate(objectInNewContext) {
|
|
||||||
var newLocationCapability = objectInNewContext
|
|
||||||
.getCapability('location'),
|
|
||||||
oldLocationCapability = object
|
|
||||||
.getCapability('location');
|
|
||||||
|
|
||||||
if (!newLocationCapability
|
|
||||||
|| !oldLocationCapability) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (oldLocationCapability.isOriginal()) {
|
|
||||||
return newLocationCapability.setPrimaryLocation(
|
|
||||||
newLocationCapability
|
|
||||||
.getContextualLocation()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.validate(object, parentObject)) {
|
|
||||||
throw new Error(
|
|
||||||
"Tried to move objects without validating first."
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.linkService
|
|
||||||
.perform(object, parentObject)
|
|
||||||
.then(relocate)
|
|
||||||
.then(function () {
|
|
||||||
return object
|
|
||||||
.getCapability('action')
|
|
||||||
.perform('remove', true);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return MoveService;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
@ -1,243 +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(
|
|
||||||
[
|
|
||||||
'../../src/actions/CopyAction',
|
|
||||||
'../services/MockCopyService',
|
|
||||||
'../DomainObjectFactory'
|
|
||||||
],
|
|
||||||
function (CopyAction, MockCopyService, domainObjectFactory) {
|
|
||||||
|
|
||||||
describe("Copy Action", function () {
|
|
||||||
|
|
||||||
var copyAction,
|
|
||||||
policyService,
|
|
||||||
locationService,
|
|
||||||
locationServicePromise,
|
|
||||||
copyService,
|
|
||||||
context,
|
|
||||||
selectedObject,
|
|
||||||
selectedObjectContextCapability,
|
|
||||||
currentParent,
|
|
||||||
newParent,
|
|
||||||
notificationService,
|
|
||||||
notification,
|
|
||||||
dialogService,
|
|
||||||
mockDialog,
|
|
||||||
mockLog,
|
|
||||||
abstractComposePromise,
|
|
||||||
domainObject = {model: {name: "mockObject"}},
|
|
||||||
progress = {
|
|
||||||
phase: "copying",
|
|
||||||
totalObjects: 10,
|
|
||||||
processed: 1
|
|
||||||
};
|
|
||||||
|
|
||||||
beforeEach(function () {
|
|
||||||
policyService = jasmine.createSpyObj(
|
|
||||||
'policyService',
|
|
||||||
['allow']
|
|
||||||
);
|
|
||||||
policyService.allow.and.returnValue(true);
|
|
||||||
|
|
||||||
selectedObjectContextCapability = jasmine.createSpyObj(
|
|
||||||
'selectedObjectContextCapability',
|
|
||||||
[
|
|
||||||
'getParent'
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
selectedObject = domainObjectFactory({
|
|
||||||
name: 'selectedObject',
|
|
||||||
model: {
|
|
||||||
name: 'selectedObject'
|
|
||||||
},
|
|
||||||
capabilities: {
|
|
||||||
context: selectedObjectContextCapability
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
currentParent = domainObjectFactory({
|
|
||||||
name: 'currentParent'
|
|
||||||
});
|
|
||||||
|
|
||||||
selectedObjectContextCapability
|
|
||||||
.getParent
|
|
||||||
.and.returnValue(currentParent);
|
|
||||||
|
|
||||||
newParent = domainObjectFactory({
|
|
||||||
name: 'newParent'
|
|
||||||
});
|
|
||||||
|
|
||||||
locationService = jasmine.createSpyObj(
|
|
||||||
'locationService',
|
|
||||||
[
|
|
||||||
'getLocationFromUser'
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
locationServicePromise = jasmine.createSpyObj(
|
|
||||||
'locationServicePromise',
|
|
||||||
[
|
|
||||||
'then'
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
abstractComposePromise = jasmine.createSpyObj(
|
|
||||||
'abstractComposePromise',
|
|
||||||
[
|
|
||||||
'then'
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
abstractComposePromise.then.and.callFake(function (success, error, notify) {
|
|
||||||
notify(progress);
|
|
||||||
success(domainObject);
|
|
||||||
});
|
|
||||||
|
|
||||||
locationServicePromise.then.and.callFake(function (callback) {
|
|
||||||
callback(newParent);
|
|
||||||
|
|
||||||
return abstractComposePromise;
|
|
||||||
});
|
|
||||||
|
|
||||||
locationService
|
|
||||||
.getLocationFromUser
|
|
||||||
.and.returnValue(locationServicePromise);
|
|
||||||
|
|
||||||
dialogService = jasmine.createSpyObj('dialogService',
|
|
||||||
['showBlockingMessage']
|
|
||||||
);
|
|
||||||
|
|
||||||
mockDialog = jasmine.createSpyObj("dialog", ["dismiss"]);
|
|
||||||
dialogService.showBlockingMessage.and.returnValue(mockDialog);
|
|
||||||
|
|
||||||
notification = jasmine.createSpyObj('notification',
|
|
||||||
['dismiss', 'model']
|
|
||||||
);
|
|
||||||
|
|
||||||
notificationService = jasmine.createSpyObj('notificationService',
|
|
||||||
['notify', 'info']
|
|
||||||
);
|
|
||||||
|
|
||||||
notificationService.notify.and.returnValue(notification);
|
|
||||||
|
|
||||||
mockLog = jasmine.createSpyObj('log', ['error']);
|
|
||||||
|
|
||||||
copyService = new MockCopyService();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("with context from context-action", function () {
|
|
||||||
beforeEach(function () {
|
|
||||||
context = {
|
|
||||||
domainObject: selectedObject
|
|
||||||
};
|
|
||||||
|
|
||||||
copyAction = new CopyAction(
|
|
||||||
mockLog,
|
|
||||||
policyService,
|
|
||||||
locationService,
|
|
||||||
copyService,
|
|
||||||
dialogService,
|
|
||||||
notificationService,
|
|
||||||
context
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("initializes happily", function () {
|
|
||||||
expect(copyAction).toBeDefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("when performed it", function () {
|
|
||||||
beforeEach(function () {
|
|
||||||
spyOn(copyAction, 'progress').and.callThrough();
|
|
||||||
copyAction.perform();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("prompts for location", function () {
|
|
||||||
expect(locationService.getLocationFromUser)
|
|
||||||
.toHaveBeenCalledWith(
|
|
||||||
"Duplicate selectedObject To a Location",
|
|
||||||
"Duplicate To",
|
|
||||||
jasmine.any(Function),
|
|
||||||
currentParent
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("waits for location and handles cancellation by user", function () {
|
|
||||||
expect(locationServicePromise.then)
|
|
||||||
.toHaveBeenCalledWith(jasmine.any(Function), jasmine.any(Function));
|
|
||||||
});
|
|
||||||
|
|
||||||
it("copies object to selected location", function () {
|
|
||||||
locationServicePromise
|
|
||||||
.then
|
|
||||||
.calls.mostRecent()
|
|
||||||
.args[0](newParent);
|
|
||||||
|
|
||||||
expect(copyService.perform)
|
|
||||||
.toHaveBeenCalledWith(selectedObject, newParent);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("notifies the user of progress", function () {
|
|
||||||
expect(notificationService.info).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("notifies the user with name of object copied", function () {
|
|
||||||
expect(notificationService.info)
|
|
||||||
.toHaveBeenCalledWith("mockObject copied successfully.");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("with context from drag-drop", function () {
|
|
||||||
beforeEach(function () {
|
|
||||||
context = {
|
|
||||||
selectedObject: selectedObject,
|
|
||||||
domainObject: newParent
|
|
||||||
};
|
|
||||||
|
|
||||||
copyAction = new CopyAction(
|
|
||||||
mockLog,
|
|
||||||
policyService,
|
|
||||||
locationService,
|
|
||||||
copyService,
|
|
||||||
dialogService,
|
|
||||||
notificationService,
|
|
||||||
context
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("initializes happily", function () {
|
|
||||||
expect(copyAction).toBeDefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("performs copy immediately", function () {
|
|
||||||
copyAction.perform();
|
|
||||||
expect(copyService.perform)
|
|
||||||
.toHaveBeenCalledWith(selectedObject, newParent);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
);
|
|
@ -1,178 +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(
|
|
||||||
[
|
|
||||||
'../../src/actions/MoveAction',
|
|
||||||
'../services/MockMoveService',
|
|
||||||
'../DomainObjectFactory'
|
|
||||||
],
|
|
||||||
function (MoveAction, MockMoveService, domainObjectFactory) {
|
|
||||||
|
|
||||||
describe("Move Action", function () {
|
|
||||||
|
|
||||||
var moveAction,
|
|
||||||
policyService,
|
|
||||||
locationService,
|
|
||||||
locationServicePromise,
|
|
||||||
moveService,
|
|
||||||
context,
|
|
||||||
selectedObject,
|
|
||||||
selectedObjectContextCapability,
|
|
||||||
currentParent,
|
|
||||||
newParent;
|
|
||||||
|
|
||||||
beforeEach(function () {
|
|
||||||
policyService = jasmine.createSpyObj(
|
|
||||||
'policyService',
|
|
||||||
['allow']
|
|
||||||
);
|
|
||||||
policyService.allow.and.returnValue(true);
|
|
||||||
|
|
||||||
selectedObjectContextCapability = jasmine.createSpyObj(
|
|
||||||
'selectedObjectContextCapability',
|
|
||||||
[
|
|
||||||
'getParent'
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
selectedObject = domainObjectFactory({
|
|
||||||
name: 'selectedObject',
|
|
||||||
model: {
|
|
||||||
name: 'selectedObject'
|
|
||||||
},
|
|
||||||
capabilities: {
|
|
||||||
context: selectedObjectContextCapability
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
currentParent = domainObjectFactory({
|
|
||||||
name: 'currentParent'
|
|
||||||
});
|
|
||||||
|
|
||||||
selectedObjectContextCapability
|
|
||||||
.getParent
|
|
||||||
.and.returnValue(currentParent);
|
|
||||||
|
|
||||||
newParent = domainObjectFactory({
|
|
||||||
name: 'newParent'
|
|
||||||
});
|
|
||||||
|
|
||||||
locationService = jasmine.createSpyObj(
|
|
||||||
'locationService',
|
|
||||||
[
|
|
||||||
'getLocationFromUser'
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
locationServicePromise = jasmine.createSpyObj(
|
|
||||||
'locationServicePromise',
|
|
||||||
[
|
|
||||||
'then'
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
locationService
|
|
||||||
.getLocationFromUser
|
|
||||||
.and.returnValue(locationServicePromise);
|
|
||||||
|
|
||||||
moveService = new MockMoveService();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("with context from context-action", function () {
|
|
||||||
beforeEach(function () {
|
|
||||||
context = {
|
|
||||||
domainObject: selectedObject
|
|
||||||
};
|
|
||||||
|
|
||||||
moveAction = new MoveAction(
|
|
||||||
policyService,
|
|
||||||
locationService,
|
|
||||||
moveService,
|
|
||||||
context
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("initializes happily", function () {
|
|
||||||
expect(moveAction).toBeDefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("when performed it", function () {
|
|
||||||
beforeEach(function () {
|
|
||||||
moveAction.perform();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("prompts for location", function () {
|
|
||||||
expect(locationService.getLocationFromUser)
|
|
||||||
.toHaveBeenCalledWith(
|
|
||||||
"Move selectedObject To a New Location",
|
|
||||||
"Move To",
|
|
||||||
jasmine.any(Function),
|
|
||||||
currentParent
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("waits for location and handles cancellation by user", function () {
|
|
||||||
expect(locationServicePromise.then)
|
|
||||||
.toHaveBeenCalledWith(jasmine.any(Function), jasmine.any(Function));
|
|
||||||
});
|
|
||||||
|
|
||||||
it("moves object to selected location", function () {
|
|
||||||
locationServicePromise
|
|
||||||
.then
|
|
||||||
.calls.mostRecent()
|
|
||||||
.args[0](newParent);
|
|
||||||
|
|
||||||
expect(moveService.perform)
|
|
||||||
.toHaveBeenCalledWith(selectedObject, newParent);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("with context from drag-drop", function () {
|
|
||||||
beforeEach(function () {
|
|
||||||
context = {
|
|
||||||
selectedObject: selectedObject,
|
|
||||||
domainObject: newParent
|
|
||||||
};
|
|
||||||
|
|
||||||
moveAction = new MoveAction(
|
|
||||||
policyService,
|
|
||||||
locationService,
|
|
||||||
moveService,
|
|
||||||
context
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("initializes happily", function () {
|
|
||||||
expect(moveAction).toBeDefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("performs move immediately", function () {
|
|
||||||
moveAction.perform();
|
|
||||||
expect(moveService.perform)
|
|
||||||
.toHaveBeenCalledWith(selectedObject, newParent);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
);
|
|
@ -1,124 +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([
|
|
||||||
'../../src/policies/MovePolicy',
|
|
||||||
'../DomainObjectFactory'
|
|
||||||
], function (MovePolicy, domainObjectFactory) {
|
|
||||||
|
|
||||||
describe("MovePolicy", function () {
|
|
||||||
var testMetadata,
|
|
||||||
testContext,
|
|
||||||
mockDomainObject,
|
|
||||||
mockParent,
|
|
||||||
mockParentType,
|
|
||||||
mockType,
|
|
||||||
mockAction,
|
|
||||||
policy;
|
|
||||||
|
|
||||||
beforeEach(function () {
|
|
||||||
var mockContextCapability =
|
|
||||||
jasmine.createSpyObj('context', ['getParent']);
|
|
||||||
|
|
||||||
mockType =
|
|
||||||
jasmine.createSpyObj('type', ['hasFeature']);
|
|
||||||
mockParentType =
|
|
||||||
jasmine.createSpyObj('parent-type', ['hasFeature']);
|
|
||||||
|
|
||||||
testMetadata = {};
|
|
||||||
|
|
||||||
mockDomainObject = domainObjectFactory({
|
|
||||||
capabilities: {
|
|
||||||
context: mockContextCapability,
|
|
||||||
type: mockType
|
|
||||||
}
|
|
||||||
});
|
|
||||||
mockParent = domainObjectFactory({
|
|
||||||
capabilities: {
|
|
||||||
type: mockParentType
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
mockContextCapability.getParent.and.returnValue(mockParent);
|
|
||||||
|
|
||||||
mockType.hasFeature.and.callFake(function (feature) {
|
|
||||||
return feature === 'creation';
|
|
||||||
});
|
|
||||||
mockParentType.hasFeature.and.callFake(function (feature) {
|
|
||||||
return feature === 'creation';
|
|
||||||
});
|
|
||||||
|
|
||||||
mockAction = jasmine.createSpyObj('action', ['getMetadata']);
|
|
||||||
mockAction.getMetadata.and.returnValue(testMetadata);
|
|
||||||
|
|
||||||
testContext = { domainObject: mockDomainObject };
|
|
||||||
|
|
||||||
policy = new MovePolicy();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("for move actions", function () {
|
|
||||||
beforeEach(function () {
|
|
||||||
testMetadata.key = 'move';
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("when an object is non-modifiable", function () {
|
|
||||||
beforeEach(function () {
|
|
||||||
mockType.hasFeature.and.returnValue(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("disallows the action", function () {
|
|
||||||
expect(policy.allow(mockAction, testContext)).toBe(false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("when a parent is non-modifiable", function () {
|
|
||||||
beforeEach(function () {
|
|
||||||
mockParentType.hasFeature.and.returnValue(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("disallows the action", function () {
|
|
||||||
expect(policy.allow(mockAction, testContext)).toBe(false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("when an object and its parent are modifiable", function () {
|
|
||||||
it("allows the action", function () {
|
|
||||||
expect(policy.allow(mockAction, testContext)).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("for other actions", function () {
|
|
||||||
beforeEach(function () {
|
|
||||||
testMetadata.key = 'foo';
|
|
||||||
});
|
|
||||||
|
|
||||||
it("simply allows the action", function () {
|
|
||||||
expect(policy.allow(mockAction, testContext)).toBe(true);
|
|
||||||
mockType.hasFeature.and.returnValue(false);
|
|
||||||
expect(policy.allow(mockAction, testContext)).toBe(true);
|
|
||||||
mockParentType.hasFeature.and.returnValue(false);
|
|
||||||
expect(policy.allow(mockAction, testContext)).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
@ -1,96 +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(
|
|
||||||
function () {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* MockMoveService provides the same interface as the moveService,
|
|
||||||
* returning promises where it would normally do so. At it's core,
|
|
||||||
* it is a jasmine spy object, but it also tracks the promises it
|
|
||||||
* returns and provides shortcut methods for resolving those promises
|
|
||||||
* synchronously.
|
|
||||||
*
|
|
||||||
* Usage:
|
|
||||||
*
|
|
||||||
* ```javascript
|
|
||||||
* var moveService = new MockMoveService();
|
|
||||||
*
|
|
||||||
* // validate is a standard jasmine spy.
|
|
||||||
* moveService.validate.and.returnValue(true);
|
|
||||||
* var isValid = moveService.validate(object, parentCandidate);
|
|
||||||
* expect(isValid).toBe(true);
|
|
||||||
*
|
|
||||||
* // perform returns promises and tracks them.
|
|
||||||
* var whenCopied = jasmine.createSpy('whenCopied');
|
|
||||||
* moveService.perform(object, parentObject).then(whenCopied);
|
|
||||||
* expect(whenCopied).not.toHaveBeenCalled();
|
|
||||||
* moveService.perform.calls.mostRecent().resolve('someArg');
|
|
||||||
* expect(whenCopied).toHaveBeenCalledWith('someArg');
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
function MockMoveService() {
|
|
||||||
// track most recent call of a function,
|
|
||||||
// perform automatically returns
|
|
||||||
var mockMoveService = jasmine.createSpyObj(
|
|
||||||
'MockMoveService',
|
|
||||||
[
|
|
||||||
'validate',
|
|
||||||
'perform'
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
mockMoveService.perform.and.callFake(() => {
|
|
||||||
var performPromise,
|
|
||||||
callExtensions,
|
|
||||||
spy;
|
|
||||||
|
|
||||||
performPromise = jasmine.createSpyObj(
|
|
||||||
'performPromise',
|
|
||||||
['then']
|
|
||||||
);
|
|
||||||
|
|
||||||
callExtensions = {
|
|
||||||
promise: performPromise,
|
|
||||||
resolve: function (resolveWith) {
|
|
||||||
performPromise.then.calls.all().forEach(function (call) {
|
|
||||||
call.args[0](resolveWith);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
spy = mockMoveService.perform;
|
|
||||||
|
|
||||||
Object.keys(callExtensions).forEach(function (key) {
|
|
||||||
spy.calls.mostRecent()[key] = callExtensions[key];
|
|
||||||
spy.calls.all()[spy.calls.count() - 1][key] = callExtensions[key];
|
|
||||||
});
|
|
||||||
|
|
||||||
return performPromise;
|
|
||||||
});
|
|
||||||
|
|
||||||
return mockMoveService;
|
|
||||||
}
|
|
||||||
|
|
||||||
return MockMoveService;
|
|
||||||
}
|
|
||||||
);
|
|
@ -1,260 +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(
|
|
||||||
[
|
|
||||||
'../../src/services/MoveService',
|
|
||||||
'../services/MockLinkService',
|
|
||||||
'../DomainObjectFactory',
|
|
||||||
'../ControlledPromise'
|
|
||||||
],
|
|
||||||
function (
|
|
||||||
MoveService,
|
|
||||||
MockLinkService,
|
|
||||||
domainObjectFactory,
|
|
||||||
ControlledPromise
|
|
||||||
) {
|
|
||||||
|
|
||||||
xdescribe("MoveService", function () {
|
|
||||||
|
|
||||||
var moveService,
|
|
||||||
policyService,
|
|
||||||
object,
|
|
||||||
objectContextCapability,
|
|
||||||
currentParent,
|
|
||||||
parentCandidate,
|
|
||||||
linkService;
|
|
||||||
|
|
||||||
beforeEach(function () {
|
|
||||||
objectContextCapability = jasmine.createSpyObj(
|
|
||||||
'objectContextCapability',
|
|
||||||
[
|
|
||||||
'getParent'
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
object = domainObjectFactory({
|
|
||||||
name: 'object',
|
|
||||||
id: 'a',
|
|
||||||
capabilities: {
|
|
||||||
context: objectContextCapability,
|
|
||||||
type: { type: 'object' }
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
currentParent = domainObjectFactory({
|
|
||||||
name: 'currentParent',
|
|
||||||
id: 'b'
|
|
||||||
});
|
|
||||||
|
|
||||||
objectContextCapability.getParent.and.returnValue(currentParent);
|
|
||||||
|
|
||||||
parentCandidate = domainObjectFactory({
|
|
||||||
name: 'parentCandidate',
|
|
||||||
model: { composition: [] },
|
|
||||||
id: 'c',
|
|
||||||
capabilities: {
|
|
||||||
type: { type: 'parentCandidate' }
|
|
||||||
}
|
|
||||||
});
|
|
||||||
policyService = jasmine.createSpyObj(
|
|
||||||
'policyService',
|
|
||||||
['allow']
|
|
||||||
);
|
|
||||||
linkService = new MockLinkService();
|
|
||||||
policyService.allow.and.returnValue(true);
|
|
||||||
moveService = new MoveService(policyService, linkService);
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("validate", function () {
|
|
||||||
var validate;
|
|
||||||
|
|
||||||
beforeEach(function () {
|
|
||||||
validate = function () {
|
|
||||||
return moveService.validate(object, parentCandidate);
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
it("does not allow an invalid parent", function () {
|
|
||||||
parentCandidate = undefined;
|
|
||||||
expect(validate()).toBe(false);
|
|
||||||
parentCandidate = {};
|
|
||||||
expect(validate()).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("does not allow moving to current parent", function () {
|
|
||||||
parentCandidate.id = currentParent.id = 'xyz';
|
|
||||||
expect(validate()).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("does not allow moving to self", function () {
|
|
||||||
object.id = parentCandidate.id = 'xyz';
|
|
||||||
expect(validate()).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("does not allow moving to the same location", function () {
|
|
||||||
object.id = 'abc';
|
|
||||||
parentCandidate.model.composition = ['abc'];
|
|
||||||
expect(validate()).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("defers to policyService", function () {
|
|
||||||
|
|
||||||
it("calls policy service with correct args", function () {
|
|
||||||
validate();
|
|
||||||
expect(policyService.allow).toHaveBeenCalledWith(
|
|
||||||
"composition",
|
|
||||||
parentCandidate,
|
|
||||||
object
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("and returns false", function () {
|
|
||||||
policyService.allow.and.returnValue(false);
|
|
||||||
expect(validate()).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("and returns true", function () {
|
|
||||||
policyService.allow.and.returnValue(true);
|
|
||||||
expect(validate()).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("perform", function () {
|
|
||||||
|
|
||||||
var actionCapability,
|
|
||||||
locationCapability,
|
|
||||||
locationPromise,
|
|
||||||
newParent,
|
|
||||||
moveResult;
|
|
||||||
|
|
||||||
beforeEach(function () {
|
|
||||||
newParent = parentCandidate;
|
|
||||||
|
|
||||||
actionCapability = jasmine.createSpyObj(
|
|
||||||
'actionCapability',
|
|
||||||
['perform']
|
|
||||||
);
|
|
||||||
|
|
||||||
locationCapability = jasmine.createSpyObj(
|
|
||||||
'locationCapability',
|
|
||||||
[
|
|
||||||
'isOriginal',
|
|
||||||
'setPrimaryLocation',
|
|
||||||
'getContextualLocation'
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
locationPromise = new ControlledPromise();
|
|
||||||
locationCapability.setPrimaryLocation
|
|
||||||
.and.returnValue(locationPromise);
|
|
||||||
|
|
||||||
object = domainObjectFactory({
|
|
||||||
name: 'object',
|
|
||||||
capabilities: {
|
|
||||||
action: actionCapability,
|
|
||||||
location: locationCapability,
|
|
||||||
context: objectContextCapability,
|
|
||||||
type: { type: 'object' }
|
|
||||||
}
|
|
||||||
});
|
|
||||||
moveResult = moveService.perform(object, newParent);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("links object to newParent", function () {
|
|
||||||
expect(linkService.perform).toHaveBeenCalledWith(
|
|
||||||
object,
|
|
||||||
newParent
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("returns a promise", function () {
|
|
||||||
expect(moveResult.then).toEqual(jasmine.any(Function));
|
|
||||||
});
|
|
||||||
|
|
||||||
it("waits for result of link", function () {
|
|
||||||
expect(linkService.perform.calls.mostRecent().promise.then)
|
|
||||||
.toHaveBeenCalledWith(jasmine.any(Function));
|
|
||||||
});
|
|
||||||
|
|
||||||
it("throws an error when performed on invalid inputs", function () {
|
|
||||||
function perform() {
|
|
||||||
moveService.perform(object, newParent);
|
|
||||||
}
|
|
||||||
|
|
||||||
spyOn(moveService, "validate");
|
|
||||||
moveService.validate.and.returnValue(true);
|
|
||||||
expect(perform).not.toThrow();
|
|
||||||
moveService.validate.and.returnValue(false);
|
|
||||||
expect(perform).toThrow();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("when moving an original", function () {
|
|
||||||
beforeEach(function () {
|
|
||||||
locationCapability.getContextualLocation
|
|
||||||
.and.returnValue('new-location');
|
|
||||||
locationCapability.isOriginal.and.returnValue(true);
|
|
||||||
linkService.perform.calls.mostRecent().promise.resolve();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("updates location", function () {
|
|
||||||
expect(locationCapability.setPrimaryLocation)
|
|
||||||
.toHaveBeenCalledWith('new-location');
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("after location update", function () {
|
|
||||||
beforeEach(function () {
|
|
||||||
locationPromise.resolve();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("removes object from parent without user warning dialog", function () {
|
|
||||||
expect(actionCapability.perform)
|
|
||||||
.toHaveBeenCalledWith('remove', true);
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("when moving a link", function () {
|
|
||||||
beforeEach(function () {
|
|
||||||
locationCapability.isOriginal.and.returnValue(false);
|
|
||||||
linkService.perform.calls.mostRecent().promise.resolve();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("does not update location", function () {
|
|
||||||
expect(locationCapability.setPrimaryLocation)
|
|
||||||
.not
|
|
||||||
.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("removes object from parent without user warning dialog", function () {
|
|
||||||
expect(actionCapability.perform)
|
|
||||||
.toHaveBeenCalledWith('remove', true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
);
|
|
@ -19,7 +19,7 @@
|
|||||||
this source code distribution or the Licensing information page available
|
this source code distribution or the Licensing information page available
|
||||||
at runtime from the About dialog for additional information.
|
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">
|
<div class="c-clock__timezone">
|
||||||
{{clock.zone()}}
|
{{clock.zone()}}
|
||||||
</div>
|
</div>
|
||||||
|
@ -19,7 +19,7 @@
|
|||||||
this source code distribution or the Licensing information page available
|
this source code distribution or the Licensing information page available
|
||||||
at runtime from the About dialog for additional information.
|
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">
|
<div class="c-timer__controls">
|
||||||
<button ng-click="timer.clickStopButton()"
|
<button ng-click="timer.clickStopButton()"
|
||||||
ng-hide="timer.timerState == 'stopped'"
|
ng-hide="timer.timerState == 'stopped'"
|
||||||
|
@ -47,6 +47,8 @@ define([
|
|||||||
"implementation": ExportAsJSONAction,
|
"implementation": ExportAsJSONAction,
|
||||||
"category": "contextual",
|
"category": "contextual",
|
||||||
"cssClass": "icon-export",
|
"cssClass": "icon-export",
|
||||||
|
"group": "json",
|
||||||
|
"priority": 2,
|
||||||
"depends": [
|
"depends": [
|
||||||
"openmct",
|
"openmct",
|
||||||
"exportService",
|
"exportService",
|
||||||
@ -61,6 +63,8 @@ define([
|
|||||||
"implementation": ImportAsJSONAction,
|
"implementation": ImportAsJSONAction,
|
||||||
"category": "contextual",
|
"category": "contextual",
|
||||||
"cssClass": "icon-import",
|
"cssClass": "icon-import",
|
||||||
|
"group": "json",
|
||||||
|
"priority": 2,
|
||||||
"depends": [
|
"depends": [
|
||||||
"exportService",
|
"exportService",
|
||||||
"identifierService",
|
"identifierService",
|
||||||
|
@ -32,7 +32,8 @@
|
|||||||
function indexItem(id, model) {
|
function indexItem(id, model) {
|
||||||
indexedItems.push({
|
indexedItems.push({
|
||||||
id: id,
|
id: id,
|
||||||
name: model.name.toLowerCase()
|
name: model.name.toLowerCase(),
|
||||||
|
type: model.type
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -125,13 +125,12 @@ define([
|
|||||||
* @param topic the topicService.
|
* @param topic the topicService.
|
||||||
*/
|
*/
|
||||||
GenericSearchProvider.prototype.indexOnMutation = function (topic) {
|
GenericSearchProvider.prototype.indexOnMutation = function (topic) {
|
||||||
var mutationTopic = topic('mutation'),
|
let mutationTopic = topic('mutation');
|
||||||
provider = this;
|
|
||||||
|
|
||||||
mutationTopic.listen(function (mutatedObject) {
|
mutationTopic.listen(mutatedObject => {
|
||||||
var editor = mutatedObject.getCapability('editor');
|
let editor = mutatedObject.getCapability('editor');
|
||||||
if (!editor || !editor.inEditContext()) {
|
if (!editor || !editor.inEditContext()) {
|
||||||
provider.index(
|
this.index(
|
||||||
mutatedObject.getId(),
|
mutatedObject.getId(),
|
||||||
mutatedObject.getModel()
|
mutatedObject.getModel()
|
||||||
);
|
);
|
||||||
@ -147,10 +146,15 @@ define([
|
|||||||
* @param {String} id to be indexed.
|
* @param {String} id to be indexed.
|
||||||
*/
|
*/
|
||||||
GenericSearchProvider.prototype.scheduleForIndexing = function (id) {
|
GenericSearchProvider.prototype.scheduleForIndexing = function (id) {
|
||||||
if (!this.indexedIds[id] && !this.pendingIndex[id]) {
|
const identifier = objectUtils.parseKeyString(id);
|
||||||
this.indexedIds[id] = true;
|
const objectProvider = this.openmct.objects.getProvider(identifier);
|
||||||
this.pendingIndex[id] = true;
|
|
||||||
this.idsToIndex.push(id);
|
if (objectProvider === undefined || objectProvider.search === undefined) {
|
||||||
|
if (!this.indexedIds[id] && !this.pendingIndex[id]) {
|
||||||
|
this.indexedIds[id] = true;
|
||||||
|
this.pendingIndex[id] = true;
|
||||||
|
this.idsToIndex.push(id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.keepIndexing();
|
this.keepIndexing();
|
||||||
@ -262,6 +266,7 @@ define([
|
|||||||
return {
|
return {
|
||||||
id: hit.item.id,
|
id: hit.item.id,
|
||||||
model: hit.item.model,
|
model: hit.item.model,
|
||||||
|
type: hit.item.type,
|
||||||
score: hit.matchCount
|
score: hit.matchCount
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
@ -41,7 +41,8 @@
|
|||||||
indexedItems.push({
|
indexedItems.push({
|
||||||
id: id,
|
id: id,
|
||||||
vector: vector,
|
vector: vector,
|
||||||
model: model
|
model: model,
|
||||||
|
type: model.type
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
19
src/MCT.js
19
src/MCT.js
@ -46,6 +46,8 @@ define([
|
|||||||
'./api/Branding',
|
'./api/Branding',
|
||||||
'./plugins/licenses/plugin',
|
'./plugins/licenses/plugin',
|
||||||
'./plugins/remove/plugin',
|
'./plugins/remove/plugin',
|
||||||
|
'./plugins/move/plugin',
|
||||||
|
'./plugins/duplicate/plugin',
|
||||||
'vue'
|
'vue'
|
||||||
], function (
|
], function (
|
||||||
EventEmitter,
|
EventEmitter,
|
||||||
@ -73,6 +75,8 @@ define([
|
|||||||
BrandingAPI,
|
BrandingAPI,
|
||||||
LicensesPlugin,
|
LicensesPlugin,
|
||||||
RemoveActionPlugin,
|
RemoveActionPlugin,
|
||||||
|
MoveActionPlugin,
|
||||||
|
DuplicateActionPlugin,
|
||||||
Vue
|
Vue
|
||||||
) {
|
) {
|
||||||
/**
|
/**
|
||||||
@ -215,7 +219,7 @@ define([
|
|||||||
* @memberof module:openmct.MCT#
|
* @memberof module:openmct.MCT#
|
||||||
* @name objects
|
* @name objects
|
||||||
*/
|
*/
|
||||||
this.objects = new api.ObjectAPI();
|
this.objects = new api.ObjectAPI.default(this.types, this);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An interface for retrieving and interpreting telemetry data associated
|
* An interface for retrieving and interpreting telemetry data associated
|
||||||
@ -242,7 +246,11 @@ define([
|
|||||||
|
|
||||||
this.overlays = new OverlayAPI.default();
|
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();
|
this.router = new ApplicationRouter();
|
||||||
|
|
||||||
@ -259,6 +267,8 @@ define([
|
|||||||
this.install(LegacyIndicatorsPlugin());
|
this.install(LegacyIndicatorsPlugin());
|
||||||
this.install(LicensesPlugin.default());
|
this.install(LicensesPlugin.default());
|
||||||
this.install(RemoveActionPlugin.default());
|
this.install(RemoveActionPlugin.default());
|
||||||
|
this.install(MoveActionPlugin.default());
|
||||||
|
this.install(DuplicateActionPlugin.default());
|
||||||
this.install(this.plugins.FolderView());
|
this.install(this.plugins.FolderView());
|
||||||
this.install(this.plugins.Tabs());
|
this.install(this.plugins.Tabs());
|
||||||
this.install(ImageryPlugin.default());
|
this.install(ImageryPlugin.default());
|
||||||
@ -271,6 +281,9 @@ define([
|
|||||||
this.install(this.plugins.URLTimeSettingsSynchronizer());
|
this.install(this.plugins.URLTimeSettingsSynchronizer());
|
||||||
this.install(this.plugins.NotificationIndicator());
|
this.install(this.plugins.NotificationIndicator());
|
||||||
this.install(this.plugins.NewFolderAction());
|
this.install(this.plugins.NewFolderAction());
|
||||||
|
this.install(this.plugins.ViewDatumAction());
|
||||||
|
this.install(this.plugins.ObjectInterceptors());
|
||||||
|
this.install(this.plugins.NonEditableFolder());
|
||||||
}
|
}
|
||||||
|
|
||||||
MCT.prototype = Object.create(EventEmitter.prototype);
|
MCT.prototype = Object.create(EventEmitter.prototype);
|
||||||
@ -359,7 +372,7 @@ define([
|
|||||||
* MCT; if undefined, MCT will be run in the body of the document
|
* MCT; if undefined, MCT will be run in the body of the document
|
||||||
*/
|
*/
|
||||||
MCT.prototype.start = function (domElement = document.body, isHeadlessMode = false) {
|
MCT.prototype.start = function (domElement = document.body, isHeadlessMode = false) {
|
||||||
if (!this.plugins.DisplayLayout._installed) {
|
if (this.types.get('layout') === undefined) {
|
||||||
this.install(this.plugins.DisplayLayout({
|
this.install(this.plugins.DisplayLayout({
|
||||||
showAsView: ['summary-widget']
|
showAsView: ['summary-widget']
|
||||||
}));
|
}));
|
||||||
|
@ -35,5 +35,5 @@ export default function LegacyActionAdapter(openmct, legacyActions) {
|
|||||||
|
|
||||||
legacyActions.filter(contextualCategoryOnly)
|
legacyActions.filter(contextualCategoryOnly)
|
||||||
.map(LegacyAction => new LegacyContextMenuAction(openmct, LegacyAction))
|
.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.description = LegacyAction.definition.description;
|
||||||
this.cssClass = LegacyAction.definition.cssClass;
|
this.cssClass = LegacyAction.definition.cssClass;
|
||||||
this.LegacyAction = LegacyAction;
|
this.LegacyAction = LegacyAction;
|
||||||
|
this.group = LegacyAction.definition.group;
|
||||||
|
this.priority = LegacyAction.definition.priority;
|
||||||
}
|
}
|
||||||
|
|
||||||
invoke(objectPath) {
|
invoke(objectPath) {
|
||||||
|
@ -61,6 +61,7 @@ define([
|
|||||||
const newStyleObject = utils.toNewFormat(legacyObject.getModel(), legacyObject.getId());
|
const newStyleObject = utils.toNewFormat(legacyObject.getModel(), legacyObject.getId());
|
||||||
const keystring = utils.makeKeyString(newStyleObject.identifier);
|
const keystring = utils.makeKeyString(newStyleObject.identifier);
|
||||||
|
|
||||||
|
this.eventEmitter.emit(keystring + ':$_synchronize_model', newStyleObject);
|
||||||
this.eventEmitter.emit(keystring + ":*", newStyleObject);
|
this.eventEmitter.emit(keystring + ":*", newStyleObject);
|
||||||
this.eventEmitter.emit('mutation', newStyleObject);
|
this.eventEmitter.emit('mutation', newStyleObject);
|
||||||
}.bind(this);
|
}.bind(this);
|
||||||
@ -128,7 +129,7 @@ define([
|
|||||||
};
|
};
|
||||||
|
|
||||||
ObjectServiceProvider.prototype.get = function (key) {
|
ObjectServiceProvider.prototype.get = function (key) {
|
||||||
const keyString = utils.makeKeyString(key);
|
let keyString = utils.makeKeyString(key);
|
||||||
|
|
||||||
return this.objectService.getObjects([keyString])
|
return this.objectService.getObjects([keyString])
|
||||||
.then(function (results) {
|
.then(function (results) {
|
||||||
@ -138,6 +139,12 @@ define([
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
ObjectServiceProvider.prototype.superSecretFallbackSearch = function (query, options) {
|
||||||
|
const searchService = this.$injector.get('searchService');
|
||||||
|
|
||||||
|
return searchService.query(query);
|
||||||
|
};
|
||||||
|
|
||||||
// Injects new object API as a decorator so that it hijacks all requests.
|
// Injects new object API as a decorator so that it hijacks all requests.
|
||||||
// Object providers implemented on new API should just work, old API should just work, many things may break.
|
// Object providers implemented on new API should just work, old API should just work, many things may break.
|
||||||
function LegacyObjectAPIInterceptor(openmct, ROOTS, instantiate, topic, objectService) {
|
function LegacyObjectAPIInterceptor(openmct, ROOTS, instantiate, topic, objectService) {
|
||||||
|
@ -20,6 +20,8 @@
|
|||||||
* at runtime from the About dialog for additional information.
|
* at runtime from the About dialog for additional information.
|
||||||
*****************************************************************************/
|
*****************************************************************************/
|
||||||
|
|
||||||
|
import utils from 'objectUtils';
|
||||||
|
|
||||||
export default class LegacyPersistenceAdapter {
|
export default class LegacyPersistenceAdapter {
|
||||||
constructor(openmct) {
|
constructor(openmct) {
|
||||||
this.openmct = openmct;
|
this.openmct = openmct;
|
||||||
@ -33,8 +35,31 @@ export default class LegacyPersistenceAdapter {
|
|||||||
return Promise.resolve(Object.keys(this.openmct.objects.providers));
|
return Promise.resolve(Object.keys(this.openmct.objects.providers));
|
||||||
}
|
}
|
||||||
|
|
||||||
updateObject(legacyDomainObject) {
|
createObject(space, key, legacyDomainObject) {
|
||||||
return this.openmct.objects.save(legacyDomainObject.useCapability('adapter'));
|
let object = utils.toNewFormat(legacyDomainObject, {
|
||||||
|
namespace: space,
|
||||||
|
key: key
|
||||||
|
});
|
||||||
|
|
||||||
|
return this.openmct.objects.save(object);
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteObject(space, key) {
|
||||||
|
const identifier = {
|
||||||
|
namespace: space,
|
||||||
|
key: key
|
||||||
|
};
|
||||||
|
|
||||||
|
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) {
|
readObject(space, key) {
|
||||||
|
189
src/api/actions/ActionCollection.js
Normal file
189
src/api/actions/ActionCollection.js
Normal file
@ -0,0 +1,189 @@
|
|||||||
|
/*****************************************************************************
|
||||||
|
* 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, skipEnvironmentObservers) {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this.applicableActions = applicableActions;
|
||||||
|
this.openmct = openmct;
|
||||||
|
this.objectPath = objectPath;
|
||||||
|
this.view = view;
|
||||||
|
this.skipEnvironmentObservers = skipEnvironmentObservers;
|
||||||
|
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);
|
||||||
|
|
||||||
|
if (!skipEnvironmentObservers) {
|
||||||
|
this._observeObjectPath();
|
||||||
|
this.openmct.editor.on('isEditing', this._updateActions);
|
||||||
|
}
|
||||||
|
|
||||||
|
this._initializeActions();
|
||||||
|
}
|
||||||
|
|
||||||
|
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() {
|
||||||
|
super.removeAllListeners();
|
||||||
|
|
||||||
|
if (!this.skipEnvironmentObservers) {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
getActionsObject() {
|
||||||
|
return this.applicableActions;
|
||||||
|
}
|
||||||
|
|
||||||
|
_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;
|
229
src/api/actions/ActionCollectionSpec.js
Normal file
229
src/api/actions/ActionCollectionSpec.js
Normal file
@ -0,0 +1,229 @@
|
|||||||
|
/*****************************************************************************
|
||||||
|
* 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 ActionCollection from './ActionCollection';
|
||||||
|
import { createOpenMct, resetApplicationState } from '../../utils/testing';
|
||||||
|
|
||||||
|
describe('The ActionCollection', () => {
|
||||||
|
let openmct;
|
||||||
|
let actionCollection;
|
||||||
|
let mockApplicableActions;
|
||||||
|
let mockObjectPath;
|
||||||
|
let mockView;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
openmct = createOpenMct();
|
||||||
|
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: ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
openmct.objects.addProvider('', jasmine.createSpyObj('mockMutableObjectProvider', [
|
||||||
|
'create',
|
||||||
|
'update'
|
||||||
|
]));
|
||||||
|
mockView = {
|
||||||
|
getViewContext: () => {
|
||||||
|
return {
|
||||||
|
onlyAppliesToTestCase: true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
mockApplicableActions = {
|
||||||
|
'test-action-object-path': {
|
||||||
|
name: 'Test Action Object Path',
|
||||||
|
key: 'test-action-object-path',
|
||||||
|
cssClass: 'test-action-object-path',
|
||||||
|
description: 'This is a test action for object path',
|
||||||
|
group: 'action',
|
||||||
|
priority: 9,
|
||||||
|
appliesTo: (objectPath) => {
|
||||||
|
if (objectPath.length) {
|
||||||
|
return objectPath[0].type === 'fake-folder';
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
invoke: () => {
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'test-action-view': {
|
||||||
|
name: 'Test Action View',
|
||||||
|
key: 'test-action-view',
|
||||||
|
cssClass: 'test-action-view',
|
||||||
|
description: 'This is a test action for view',
|
||||||
|
group: 'action',
|
||||||
|
priority: 9,
|
||||||
|
showInStatusBar: true,
|
||||||
|
appliesTo: (objectPath, view = {}) => {
|
||||||
|
if (view.getViewContext) {
|
||||||
|
let viewContext = view.getViewContext();
|
||||||
|
|
||||||
|
return viewContext.onlyAppliesToTestCase;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
invoke: () => {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
actionCollection = new ActionCollection(mockApplicableActions, mockObjectPath, mockView, openmct);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
actionCollection.destroy();
|
||||||
|
resetApplicationState(openmct);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("disable method invoked with action keys", () => {
|
||||||
|
it("marks those actions as isDisabled", () => {
|
||||||
|
let actionKey = 'test-action-object-path';
|
||||||
|
let actionsObject = actionCollection.getActionsObject();
|
||||||
|
let action = actionsObject[actionKey];
|
||||||
|
|
||||||
|
expect(action.isDisabled).toBeFalsy();
|
||||||
|
|
||||||
|
actionCollection.disable([actionKey]);
|
||||||
|
actionsObject = actionCollection.getActionsObject();
|
||||||
|
action = actionsObject[actionKey];
|
||||||
|
|
||||||
|
expect(action.isDisabled).toBeTrue();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("enable method invoked with action keys", () => {
|
||||||
|
it("marks the isDisabled property as false", () => {
|
||||||
|
let actionKey = 'test-action-object-path';
|
||||||
|
|
||||||
|
actionCollection.disable([actionKey]);
|
||||||
|
|
||||||
|
let actionsObject = actionCollection.getActionsObject();
|
||||||
|
let action = actionsObject[actionKey];
|
||||||
|
|
||||||
|
expect(action.isDisabled).toBeTrue();
|
||||||
|
|
||||||
|
actionCollection.enable([actionKey]);
|
||||||
|
actionsObject = actionCollection.getActionsObject();
|
||||||
|
action = actionsObject[actionKey];
|
||||||
|
|
||||||
|
expect(action.isDisabled).toBeFalse();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("hide method invoked with action keys", () => {
|
||||||
|
it("marks those actions as isHidden", () => {
|
||||||
|
let actionKey = 'test-action-object-path';
|
||||||
|
let actionsObject = actionCollection.getActionsObject();
|
||||||
|
let action = actionsObject[actionKey];
|
||||||
|
|
||||||
|
expect(action.isHidden).toBeFalsy();
|
||||||
|
|
||||||
|
actionCollection.hide([actionKey]);
|
||||||
|
actionsObject = actionCollection.getActionsObject();
|
||||||
|
action = actionsObject[actionKey];
|
||||||
|
|
||||||
|
expect(action.isHidden).toBeTrue();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("show method invoked with action keys", () => {
|
||||||
|
it("marks the isHidden property as false", () => {
|
||||||
|
let actionKey = 'test-action-object-path';
|
||||||
|
|
||||||
|
actionCollection.hide([actionKey]);
|
||||||
|
|
||||||
|
let actionsObject = actionCollection.getActionsObject();
|
||||||
|
let action = actionsObject[actionKey];
|
||||||
|
|
||||||
|
expect(action.isHidden).toBeTrue();
|
||||||
|
|
||||||
|
actionCollection.show([actionKey]);
|
||||||
|
actionsObject = actionCollection.getActionsObject();
|
||||||
|
action = actionsObject[actionKey];
|
||||||
|
|
||||||
|
expect(action.isHidden).toBeFalse();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("getVisibleActions method", () => {
|
||||||
|
it("returns an array of non hidden actions", () => {
|
||||||
|
let action1Key = 'test-action-object-path';
|
||||||
|
let action2Key = 'test-action-view';
|
||||||
|
|
||||||
|
actionCollection.hide([action1Key]);
|
||||||
|
|
||||||
|
let visibleActions = actionCollection.getVisibleActions();
|
||||||
|
|
||||||
|
expect(Array.isArray(visibleActions)).toBeTrue();
|
||||||
|
expect(visibleActions.length).toEqual(1);
|
||||||
|
expect(visibleActions[0].key).toEqual(action2Key);
|
||||||
|
|
||||||
|
actionCollection.show([action1Key]);
|
||||||
|
visibleActions = actionCollection.getVisibleActions();
|
||||||
|
|
||||||
|
expect(visibleActions.length).toEqual(2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("getStatusBarActions method", () => {
|
||||||
|
it("returns an array of non disabled, non hidden statusBar actions", () => {
|
||||||
|
let action2Key = 'test-action-view';
|
||||||
|
|
||||||
|
let statusBarActions = actionCollection.getStatusBarActions();
|
||||||
|
|
||||||
|
expect(Array.isArray(statusBarActions)).toBeTrue();
|
||||||
|
expect(statusBarActions.length).toEqual(1);
|
||||||
|
expect(statusBarActions[0].key).toEqual(action2Key);
|
||||||
|
|
||||||
|
actionCollection.disable([action2Key]);
|
||||||
|
statusBarActions = actionCollection.getStatusBarActions();
|
||||||
|
|
||||||
|
expect(statusBarActions.length).toEqual(0);
|
||||||
|
|
||||||
|
actionCollection.enable([action2Key]);
|
||||||
|
statusBarActions = actionCollection.getStatusBarActions();
|
||||||
|
|
||||||
|
expect(statusBarActions.length).toEqual(1);
|
||||||
|
expect(statusBarActions[0].key).toEqual(action2Key);
|
||||||
|
|
||||||
|
actionCollection.hide([action2Key]);
|
||||||
|
statusBarActions = actionCollection.getStatusBarActions();
|
||||||
|
|
||||||
|
expect(statusBarActions.length).toEqual(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
144
src/api/actions/ActionsAPI.js
Normal file
144
src/api/actions/ActionsAPI.js
Normal file
@ -0,0 +1,144 @@
|
|||||||
|
/*****************************************************************************
|
||||||
|
* 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) {
|
||||||
|
if (view) {
|
||||||
|
|
||||||
|
return this._getCachedActionCollection(objectPath, view) || this._newActionCollection(objectPath, view, true);
|
||||||
|
} else {
|
||||||
|
|
||||||
|
return this._newActionCollection(objectPath, view, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateGroupOrder(groupArray) {
|
||||||
|
this._groupOrder = groupArray;
|
||||||
|
}
|
||||||
|
|
||||||
|
_get(objectPath, view) {
|
||||||
|
let actionCollection = this._newActionCollection(objectPath, view);
|
||||||
|
|
||||||
|
this._actionCollections.set(view, actionCollection);
|
||||||
|
actionCollection.on('destroy', this._updateCachedActionCollections);
|
||||||
|
|
||||||
|
return actionCollection;
|
||||||
|
}
|
||||||
|
|
||||||
|
_getCachedActionCollection(objectPath, view) {
|
||||||
|
let cachedActionCollection = this._actionCollections.get(view);
|
||||||
|
|
||||||
|
return cachedActionCollection;
|
||||||
|
}
|
||||||
|
|
||||||
|
_newActionCollection(objectPath, view, skipEnvironmentObservers) {
|
||||||
|
let applicableActions = this._applicableActions(objectPath, view);
|
||||||
|
|
||||||
|
return new ActionCollection(applicableActions, objectPath, view, this._openmct, skipEnvironmentObservers);
|
||||||
|
}
|
||||||
|
|
||||||
|
_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;
|
153
src/api/actions/ActionsAPISpec.js
Normal file
153
src/api/actions/ActionsAPISpec.js
Normal file
@ -0,0 +1,153 @@
|
|||||||
|
/*****************************************************************************
|
||||||
|
* 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';
|
||||||
|
import ActionCollection from './ActionCollection';
|
||||||
|
|
||||||
|
describe('The Actions API', () => {
|
||||||
|
let openmct;
|
||||||
|
let actionsAPI;
|
||||||
|
let mockAction;
|
||||||
|
let mockObjectPath;
|
||||||
|
let mockObjectPathAction;
|
||||||
|
let mockViewContext1;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
openmct = createOpenMct();
|
||||||
|
actionsAPI = new ActionsAPI(openmct);
|
||||||
|
mockObjectPathAction = {
|
||||||
|
name: 'Test Action Object Path',
|
||||||
|
key: 'test-action-object-path',
|
||||||
|
cssClass: 'test-action-object-path',
|
||||||
|
description: 'This is a test action for object path',
|
||||||
|
group: 'action',
|
||||||
|
priority: 9,
|
||||||
|
appliesTo: (objectPath) => {
|
||||||
|
if (objectPath.length) {
|
||||||
|
return objectPath[0].type === 'fake-folder';
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
invoke: () => {
|
||||||
|
}
|
||||||
|
};
|
||||||
|
mockAction = {
|
||||||
|
name: 'Test Action View',
|
||||||
|
key: 'test-action-view',
|
||||||
|
cssClass: 'test-action-view',
|
||||||
|
description: 'This is a test action for view',
|
||||||
|
group: 'action',
|
||||||
|
priority: 9,
|
||||||
|
appliesTo: (objectPath, view = {}) => {
|
||||||
|
if (view.getViewContext) {
|
||||||
|
let viewContext = view.getViewContext();
|
||||||
|
|
||||||
|
return viewContext.onlyAppliesToTestCase;
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
resetApplicationState(openmct);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("register method", () => {
|
||||||
|
it("adds action to ActionsAPI", () => {
|
||||||
|
actionsAPI.register(mockAction);
|
||||||
|
|
||||||
|
let actionCollection = actionsAPI.get(mockObjectPath, mockViewContext1);
|
||||||
|
let action = actionCollection.getActionsObject()[mockAction.key];
|
||||||
|
|
||||||
|
expect(action.key).toEqual(mockAction.key);
|
||||||
|
expect(action.name).toEqual(mockAction.name);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("get method", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
actionsAPI.register(mockAction);
|
||||||
|
actionsAPI.register(mockObjectPathAction);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns an ActionCollection when invoked with an objectPath only", () => {
|
||||||
|
let actionCollection = actionsAPI.get(mockObjectPath);
|
||||||
|
let instanceOfActionCollection = actionCollection instanceof ActionCollection;
|
||||||
|
|
||||||
|
expect(instanceOfActionCollection).toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns an ActionCollection when invoked with an objectPath and view", () => {
|
||||||
|
let actionCollection = actionsAPI.get(mockObjectPath, mockViewContext1);
|
||||||
|
let instanceOfActionCollection = actionCollection instanceof ActionCollection;
|
||||||
|
|
||||||
|
expect(instanceOfActionCollection).toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns relevant actions when invoked with objectPath only", () => {
|
||||||
|
let actionCollection = actionsAPI.get(mockObjectPath);
|
||||||
|
let action = actionCollection.getActionsObject()[mockObjectPathAction.key];
|
||||||
|
|
||||||
|
expect(action.key).toEqual(mockObjectPathAction.key);
|
||||||
|
expect(action.name).toEqual(mockObjectPathAction.name);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns relevant actions when invoked with objectPath and view", () => {
|
||||||
|
let actionCollection = actionsAPI.get(mockObjectPath, mockViewContext1);
|
||||||
|
let action = actionCollection.getActionsObject()[mockAction.key];
|
||||||
|
|
||||||
|
expect(action.key).toEqual(mockAction.key);
|
||||||
|
expect(action.name).toEqual(mockAction.name);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -28,9 +28,10 @@ define([
|
|||||||
'./telemetry/TelemetryAPI',
|
'./telemetry/TelemetryAPI',
|
||||||
'./indicators/IndicatorAPI',
|
'./indicators/IndicatorAPI',
|
||||||
'./notifications/NotificationAPI',
|
'./notifications/NotificationAPI',
|
||||||
'./contextMenu/ContextMenuAPI',
|
'./Editor',
|
||||||
'./Editor'
|
'./menu/MenuAPI',
|
||||||
|
'./actions/ActionsAPI',
|
||||||
|
'./status/StatusAPI'
|
||||||
], function (
|
], function (
|
||||||
TimeAPI,
|
TimeAPI,
|
||||||
ObjectAPI,
|
ObjectAPI,
|
||||||
@ -39,8 +40,10 @@ define([
|
|||||||
TelemetryAPI,
|
TelemetryAPI,
|
||||||
IndicatorAPI,
|
IndicatorAPI,
|
||||||
NotificationAPI,
|
NotificationAPI,
|
||||||
ContextMenuAPI,
|
EditorAPI,
|
||||||
EditorAPI
|
MenuAPI,
|
||||||
|
ActionsAPI,
|
||||||
|
StatusAPI
|
||||||
) {
|
) {
|
||||||
return {
|
return {
|
||||||
TimeAPI: TimeAPI,
|
TimeAPI: TimeAPI,
|
||||||
@ -51,6 +54,8 @@ define([
|
|||||||
IndicatorAPI: IndicatorAPI,
|
IndicatorAPI: IndicatorAPI,
|
||||||
NotificationAPI: NotificationAPI.default,
|
NotificationAPI: NotificationAPI.default,
|
||||||
EditorAPI: EditorAPI,
|
EditorAPI: EditorAPI,
|
||||||
ContextMenuRegistry: ContextMenuAPI.default
|
MenuAPI: MenuAPI.default,
|
||||||
|
ActionsAPI: ActionsAPI.default,
|
||||||
|
StatusAPI: StatusAPI.default
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
@ -60,6 +60,17 @@ define([
|
|||||||
};
|
};
|
||||||
this.onProviderAdd = this.onProviderAdd.bind(this);
|
this.onProviderAdd = this.onProviderAdd.bind(this);
|
||||||
this.onProviderRemove = this.onProviderRemove.bind(this);
|
this.onProviderRemove = this.onProviderRemove.bind(this);
|
||||||
|
this.mutables = {};
|
||||||
|
|
||||||
|
if (this.domainObject.isMutable) {
|
||||||
|
this.returnMutables = true;
|
||||||
|
let unobserve = this.domainObject.$on('$_destroy', () => {
|
||||||
|
Object.values(this.mutables).forEach(mutable => {
|
||||||
|
this.publicAPI.objects.destroyMutable(mutable);
|
||||||
|
});
|
||||||
|
unobserve();
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -75,10 +86,6 @@ define([
|
|||||||
throw new Error('Event not supported by composition: ' + event);
|
throw new Error('Event not supported by composition: ' + event);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.mutationListener) {
|
|
||||||
this._synchronize();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.provider.on && this.provider.off) {
|
if (this.provider.on && this.provider.off) {
|
||||||
if (event === 'add') {
|
if (event === 'add') {
|
||||||
this.provider.on(
|
this.provider.on(
|
||||||
@ -189,6 +196,13 @@ define([
|
|||||||
|
|
||||||
this.provider.add(this.domainObject, child.identifier);
|
this.provider.add(this.domainObject, child.identifier);
|
||||||
} else {
|
} else {
|
||||||
|
if (this.returnMutables && this.publicAPI.objects.supportsMutation(child.identifier)) {
|
||||||
|
let keyString = this.publicAPI.objects.makeKeyString(child.identifier);
|
||||||
|
|
||||||
|
child = this.publicAPI.objects._toMutable(child);
|
||||||
|
this.mutables[keyString] = child;
|
||||||
|
}
|
||||||
|
|
||||||
this.emit('add', child);
|
this.emit('add', child);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -202,6 +216,8 @@ define([
|
|||||||
* @name load
|
* @name load
|
||||||
*/
|
*/
|
||||||
CompositionCollection.prototype.load = function () {
|
CompositionCollection.prototype.load = function () {
|
||||||
|
this.cleanUpMutables();
|
||||||
|
|
||||||
return this.provider.load(this.domainObject)
|
return this.provider.load(this.domainObject)
|
||||||
.then(function (children) {
|
.then(function (children) {
|
||||||
return Promise.all(children.map((c) => this.publicAPI.objects.get(c)));
|
return Promise.all(children.map((c) => this.publicAPI.objects.get(c)));
|
||||||
@ -234,6 +250,14 @@ define([
|
|||||||
if (!skipMutate) {
|
if (!skipMutate) {
|
||||||
this.provider.remove(this.domainObject, child.identifier);
|
this.provider.remove(this.domainObject, child.identifier);
|
||||||
} else {
|
} else {
|
||||||
|
if (this.returnMutables) {
|
||||||
|
let keyString = this.publicAPI.objects.makeKeyString(child);
|
||||||
|
if (this.mutables[keyString] !== undefined && this.mutables[keyString].isMutable) {
|
||||||
|
this.publicAPI.objects.destroyMutable(this.mutables[keyString]);
|
||||||
|
delete this.mutables[keyString];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this.emit('remove', child);
|
this.emit('remove', child);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -281,12 +305,6 @@ define([
|
|||||||
this.remove(child, true);
|
this.remove(child, true);
|
||||||
};
|
};
|
||||||
|
|
||||||
CompositionCollection.prototype._synchronize = function () {
|
|
||||||
this.mutationListener = this.publicAPI.objects.observe(this.domainObject, '*', (newDomainObject) => {
|
|
||||||
this.domainObject = JSON.parse(JSON.stringify(newDomainObject));
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
CompositionCollection.prototype._destroy = function () {
|
CompositionCollection.prototype._destroy = function () {
|
||||||
if (this.mutationListener) {
|
if (this.mutationListener) {
|
||||||
this.mutationListener();
|
this.mutationListener();
|
||||||
@ -308,5 +326,11 @@ define([
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
CompositionCollection.prototype.cleanUpMutables = function () {
|
||||||
|
Object.values(this.mutables).forEach(mutable => {
|
||||||
|
this.publicAPI.objects.destroyMutable(mutable);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
return CompositionCollection;
|
return CompositionCollection;
|
||||||
});
|
});
|
||||||
|
@ -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;
|
|
68
src/api/menu/MenuAPI.js
Normal file
68
src/api/menu/MenuAPI.js
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
/*****************************************************************************
|
||||||
|
* 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, onDestroy) {
|
||||||
|
if (this.menuComponent) {
|
||||||
|
this.menuComponent.dismiss();
|
||||||
|
}
|
||||||
|
|
||||||
|
let options = {
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
actions,
|
||||||
|
onDestroy
|
||||||
|
};
|
||||||
|
|
||||||
|
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;
|
134
src/api/menu/MenuAPISpec.js
Normal file
134
src/api/menu/MenuAPISpec.js
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
/*****************************************************************************
|
||||||
|
* 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;
|
||||||
|
let onDestroy;
|
||||||
|
|
||||||
|
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(() => {
|
||||||
|
onDestroy = jasmine.createSpy('onDestroy');
|
||||||
|
|
||||||
|
menuAPI.showMenu(x, y, actionsArray, onDestroy);
|
||||||
|
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();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("invokes the onDestroy callback if passed in", () => {
|
||||||
|
document.body.click();
|
||||||
|
|
||||||
|
expect(onDestroy).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({
|
||||||
|
components: {
|
||||||
|
MenuComponent
|
||||||
|
},
|
||||||
|
provide: {
|
||||||
|
actions: options.actions
|
||||||
|
},
|
||||||
|
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;
|
@ -75,13 +75,20 @@ export default class NotificationAPI extends EventEmitter {
|
|||||||
* Info notifications are low priority informational messages for the user. They will be auto-destroy after a brief
|
* Info notifications are low priority informational messages for the user. They will be auto-destroy after a brief
|
||||||
* period of time.
|
* period of time.
|
||||||
* @param {string} message The message to display to the user
|
* @param {string} message The message to display to the user
|
||||||
|
* @param {Object} [options] object with following properties
|
||||||
|
* autoDismissTimeout: {number} in miliseconds to automatically dismisses notification
|
||||||
|
* link: {Object} Add a link to notifications for navigation
|
||||||
|
* onClick: callback function
|
||||||
|
* cssClass: css class name to add style on link
|
||||||
|
* text: text to display for link
|
||||||
* @returns {InfoNotification}
|
* @returns {InfoNotification}
|
||||||
*/
|
*/
|
||||||
info(message) {
|
info(message, options = {}) {
|
||||||
let notificationModel = {
|
let notificationModel = {
|
||||||
message: message,
|
message: message,
|
||||||
autoDismiss: true,
|
autoDismiss: true,
|
||||||
severity: "info"
|
severity: "info",
|
||||||
|
options
|
||||||
};
|
};
|
||||||
|
|
||||||
return this._notify(notificationModel);
|
return this._notify(notificationModel);
|
||||||
@ -90,12 +97,19 @@ export default class NotificationAPI extends EventEmitter {
|
|||||||
/**
|
/**
|
||||||
* Present an alert to the user.
|
* Present an alert to the user.
|
||||||
* @param {string} message The message to display to the user.
|
* @param {string} message The message to display to the user.
|
||||||
|
* @param {Object} [options] object with following properties
|
||||||
|
* autoDismissTimeout: {number} in miliseconds to automatically dismisses notification
|
||||||
|
* link: {Object} Add a link to notifications for navigation
|
||||||
|
* onClick: callback function
|
||||||
|
* cssClass: css class name to add style on link
|
||||||
|
* text: text to display for link
|
||||||
* @returns {Notification}
|
* @returns {Notification}
|
||||||
*/
|
*/
|
||||||
alert(message) {
|
alert(message, options = {}) {
|
||||||
let notificationModel = {
|
let notificationModel = {
|
||||||
message: message,
|
message: message,
|
||||||
severity: "alert"
|
severity: "alert",
|
||||||
|
options
|
||||||
};
|
};
|
||||||
|
|
||||||
return this._notify(notificationModel);
|
return this._notify(notificationModel);
|
||||||
@ -104,12 +118,19 @@ export default class NotificationAPI extends EventEmitter {
|
|||||||
/**
|
/**
|
||||||
* Present an error message to the user
|
* Present an error message to the user
|
||||||
* @param {string} message
|
* @param {string} message
|
||||||
|
* @param {Object} [options] object with following properties
|
||||||
|
* autoDismissTimeout: {number} in miliseconds to automatically dismisses notification
|
||||||
|
* link: {Object} Add a link to notifications for navigation
|
||||||
|
* onClick: callback function
|
||||||
|
* cssClass: css class name to add style on link
|
||||||
|
* text: text to display for link
|
||||||
* @returns {Notification}
|
* @returns {Notification}
|
||||||
*/
|
*/
|
||||||
error(message) {
|
error(message, options = {}) {
|
||||||
let notificationModel = {
|
let notificationModel = {
|
||||||
message: message,
|
message: message,
|
||||||
severity: "error"
|
severity: "error",
|
||||||
|
options
|
||||||
};
|
};
|
||||||
|
|
||||||
return this._notify(notificationModel);
|
return this._notify(notificationModel);
|
||||||
@ -325,9 +346,11 @@ export default class NotificationAPI extends EventEmitter {
|
|||||||
this.emit('notification', notification);
|
this.emit('notification', notification);
|
||||||
|
|
||||||
if (notification.model.autoDismiss || this._selectNextNotification()) {
|
if (notification.model.autoDismiss || this._selectNextNotification()) {
|
||||||
|
const autoDismissTimeout = notification.model.options.autoDismissTimeout
|
||||||
|
|| DEFAULT_AUTO_DISMISS_TIMEOUT;
|
||||||
this.activeTimeout = setTimeout(() => {
|
this.activeTimeout = setTimeout(() => {
|
||||||
this._dismissOrMinimize(notification);
|
this._dismissOrMinimize(notification);
|
||||||
}, DEFAULT_AUTO_DISMISS_TIMEOUT);
|
}, autoDismissTimeout);
|
||||||
} else {
|
} else {
|
||||||
delete this.activeTimeout;
|
delete this.activeTimeout;
|
||||||
}
|
}
|
||||||
|
154
src/api/notifications/NotificationAPISpec.js
Normal file
154
src/api/notifications/NotificationAPISpec.js
Normal file
@ -0,0 +1,154 @@
|
|||||||
|
/*****************************************************************************
|
||||||
|
* 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 NotificationAPI from './NotificationAPI';
|
||||||
|
|
||||||
|
describe('The Notifiation API', () => {
|
||||||
|
let notificationAPIInstance;
|
||||||
|
let defaultTimeout = 4000;
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
notificationAPIInstance = new NotificationAPI();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('the info method', () => {
|
||||||
|
let message = 'Example Notification Message';
|
||||||
|
let severity = 'info';
|
||||||
|
let notificationModel;
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
notificationModel = notificationAPIInstance.info(message).model;
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
notificationAPIInstance.dismissAllNotifications();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows a string message with info severity', () => {
|
||||||
|
expect(notificationModel.message).toEqual(message);
|
||||||
|
expect(notificationModel.severity).toEqual(severity);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('auto dismisses the notification after a brief timeout', (done) => {
|
||||||
|
window.setTimeout(() => {
|
||||||
|
expect(notificationAPIInstance.notifications.length).toEqual(0);
|
||||||
|
done();
|
||||||
|
}, defaultTimeout);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('the alert method', () => {
|
||||||
|
let message = 'Example alert message';
|
||||||
|
let severity = 'alert';
|
||||||
|
let notificationModel;
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
notificationModel = notificationAPIInstance.alert(message).model;
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
notificationAPIInstance.dismissAllNotifications();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows a string message, with alert severity', () => {
|
||||||
|
expect(notificationModel.message).toEqual(message);
|
||||||
|
expect(notificationModel.severity).toEqual(severity);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not auto dismiss the notification', (done) => {
|
||||||
|
window.setTimeout(() => {
|
||||||
|
expect(notificationAPIInstance.notifications.length).toEqual(1);
|
||||||
|
done();
|
||||||
|
}, defaultTimeout);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('the error method', () => {
|
||||||
|
let message = 'Example error message';
|
||||||
|
let severity = 'error';
|
||||||
|
let notificationModel;
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
notificationModel = notificationAPIInstance.error(message).model;
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
notificationAPIInstance.dismissAllNotifications();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows a string message, with severity error', () => {
|
||||||
|
expect(notificationModel.message).toEqual(message);
|
||||||
|
expect(notificationModel.severity).toEqual(severity);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not auto dismiss the notification', (done) => {
|
||||||
|
window.setTimeout(() => {
|
||||||
|
expect(notificationAPIInstance.notifications.length).toEqual(1);
|
||||||
|
done();
|
||||||
|
}, defaultTimeout);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('the progress method', () => {
|
||||||
|
let title = 'This is a progress notification';
|
||||||
|
let message1 = 'Example progress message 1';
|
||||||
|
let message2 = 'Example progress message 2';
|
||||||
|
let percentage1 = 50;
|
||||||
|
let percentage2 = 99.9;
|
||||||
|
let severity = 'info';
|
||||||
|
let notification;
|
||||||
|
let updatedPercentage;
|
||||||
|
let updatedMessage;
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
notification = notificationAPIInstance.progress(title, percentage1, message1);
|
||||||
|
notification.on('progress', (percentage, text) => {
|
||||||
|
updatedPercentage = percentage;
|
||||||
|
updatedMessage = text;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
notificationAPIInstance.dismissAllNotifications();
|
||||||
|
});
|
||||||
|
|
||||||
|
it ('shows a notification with a message, progress message, percentage and info severity', () => {
|
||||||
|
expect(notification.model.message).toEqual(title);
|
||||||
|
expect(notification.model.severity).toEqual(severity);
|
||||||
|
expect(notification.model.progressText).toEqual(message1);
|
||||||
|
expect(notification.model.progressPerc).toEqual(percentage1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it ('allows dynamically updating the progress attributes', () => {
|
||||||
|
notification.progress(percentage2, message2);
|
||||||
|
|
||||||
|
expect(updatedPercentage).toEqual(percentage2);
|
||||||
|
expect(updatedMessage).toEqual(message2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it ('allows dynamically dismissing of progress notification', () => {
|
||||||
|
notification.dismiss();
|
||||||
|
|
||||||
|
expect(notificationAPIInstance.notifications.length).toEqual(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
66
src/api/objects/InterceptorRegistry.js
Normal file
66
src/api/objects/InterceptorRegistry.js
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
/*****************************************************************************
|
||||||
|
* 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.
|
||||||
|
*****************************************************************************/
|
||||||
|
export default class InterceptorRegistry {
|
||||||
|
/**
|
||||||
|
* A InterceptorRegistry maintains the definitions for different interceptors that may be invoked on domain objects.
|
||||||
|
* @interface InterceptorRegistry
|
||||||
|
* @memberof module:openmct
|
||||||
|
*/
|
||||||
|
constructor() {
|
||||||
|
this.interceptors = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @interface InterceptorDef
|
||||||
|
* @property {function} appliesTo function that determines if this interceptor should be called for the given identifier/object
|
||||||
|
* @property {function} invoke function that transforms the provided domain object and returns the transformed domain object
|
||||||
|
* @property {function} priority the priority for this interceptor. A higher number returned has more weight than a lower number
|
||||||
|
* @memberof module:openmct InterceptorRegistry#
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register a new object interceptor.
|
||||||
|
*
|
||||||
|
* @param {module:openmct.InterceptorDef} interceptorDef the interceptor to add
|
||||||
|
* @method addInterceptor
|
||||||
|
* @memberof module:openmct.InterceptorRegistry#
|
||||||
|
*/
|
||||||
|
addInterceptor(interceptorDef) {
|
||||||
|
//TODO: sort by priority
|
||||||
|
this.interceptors.push(interceptorDef);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve all interceptors applicable to a domain object.
|
||||||
|
* @method getInterceptors
|
||||||
|
* @returns [module:openmct.InterceptorDef] the registered interceptors for this identifier/object
|
||||||
|
* @memberof module:openmct.InterceptorRegistry#
|
||||||
|
*/
|
||||||
|
getInterceptors(identifier, object) {
|
||||||
|
return this.interceptors.filter(interceptor => {
|
||||||
|
return typeof interceptor.appliesTo === 'function'
|
||||||
|
&& interceptor.appliesTo(identifier, object);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
0
src/api/objects/InterceptorRegistrySpec.js
Normal file
0
src/api/objects/InterceptorRegistrySpec.js
Normal file
147
src/api/objects/MutableDomainObject.js
Normal file
147
src/api/objects/MutableDomainObject.js
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
/*****************************************************************************
|
||||||
|
* 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 _ from 'lodash';
|
||||||
|
import utils from './object-utils.js';
|
||||||
|
import EventEmitter from 'EventEmitter';
|
||||||
|
|
||||||
|
const ANY_OBJECT_EVENT = 'mutation';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wraps a domain object to keep its model synchronized with other instances of the same object.
|
||||||
|
*
|
||||||
|
* Creating a MutableDomainObject will automatically register listeners to keep its model in sync. As such, developers
|
||||||
|
* should be careful to destroy MutableDomainObject in order to avoid memory leaks.
|
||||||
|
*
|
||||||
|
* All Open MCT API functions that provide objects will provide MutableDomainObjects where possible, except
|
||||||
|
* `openmct.objects.get()`, and will manage that object's lifecycle for you. Calling `openmct.objects.getMutable()`
|
||||||
|
* will result in the creation of a new MutableDomainObject and you will be responsible for destroying it
|
||||||
|
* (via openmct.objects.destroy) when you're done with it.
|
||||||
|
*
|
||||||
|
* @typedef MutableDomainObject
|
||||||
|
* @memberof module:openmct
|
||||||
|
*/
|
||||||
|
class MutableDomainObject {
|
||||||
|
constructor(eventEmitter) {
|
||||||
|
Object.defineProperties(this, {
|
||||||
|
_globalEventEmitter: {
|
||||||
|
value: eventEmitter,
|
||||||
|
// Property should not be serialized
|
||||||
|
enumerable: false
|
||||||
|
},
|
||||||
|
_instanceEventEmitter: {
|
||||||
|
value: new EventEmitter(),
|
||||||
|
// Property should not be serialized
|
||||||
|
enumerable: false
|
||||||
|
},
|
||||||
|
_observers: {
|
||||||
|
value: [],
|
||||||
|
// Property should not be serialized
|
||||||
|
enumerable: false
|
||||||
|
},
|
||||||
|
isMutable: {
|
||||||
|
value: true,
|
||||||
|
// Property should not be serialized
|
||||||
|
enumerable: false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
$observe(path, callback) {
|
||||||
|
let fullPath = qualifiedEventName(this, path);
|
||||||
|
let eventOff =
|
||||||
|
this._globalEventEmitter.off.bind(this._globalEventEmitter, fullPath, callback);
|
||||||
|
|
||||||
|
this._globalEventEmitter.on(fullPath, callback);
|
||||||
|
this._observers.push(eventOff);
|
||||||
|
|
||||||
|
return eventOff;
|
||||||
|
}
|
||||||
|
$set(path, value) {
|
||||||
|
_.set(this, path, value);
|
||||||
|
_.set(this, 'modified', Date.now());
|
||||||
|
|
||||||
|
//Emit secret synchronization event first, so that all objects are in sync before subsequent events fired.
|
||||||
|
this._globalEventEmitter.emit(qualifiedEventName(this, '$_synchronize_model'), this);
|
||||||
|
|
||||||
|
//Emit a general "any object" event
|
||||||
|
this._globalEventEmitter.emit(ANY_OBJECT_EVENT, this);
|
||||||
|
//Emit wildcard event, with path so that callback knows what changed
|
||||||
|
this._globalEventEmitter.emit(qualifiedEventName(this, '*'), this, path, value);
|
||||||
|
|
||||||
|
//Emit events specific to properties affected
|
||||||
|
let parentPropertiesList = path.split('.');
|
||||||
|
for (let index = parentPropertiesList.length; index > 0; index--) {
|
||||||
|
let parentPropertyPath = parentPropertiesList.slice(0, index).join('.');
|
||||||
|
this._globalEventEmitter.emit(qualifiedEventName(this, parentPropertyPath), _.get(this, parentPropertyPath));
|
||||||
|
}
|
||||||
|
|
||||||
|
//TODO: Emit events for listeners of child properties when parent changes.
|
||||||
|
// Do it at observer time - also register observers for parent attribute path.
|
||||||
|
}
|
||||||
|
|
||||||
|
$refresh(model) {
|
||||||
|
//TODO: Currently we are updating the entire object.
|
||||||
|
// In the future we could update a specific property of the object using the 'path' parameter.
|
||||||
|
this._globalEventEmitter.emit(qualifiedEventName(this, '$_synchronize_model'), model);
|
||||||
|
|
||||||
|
//Emit wildcard event, with path so that callback knows what changed
|
||||||
|
this._globalEventEmitter.emit(qualifiedEventName(this, '*'), this);
|
||||||
|
}
|
||||||
|
|
||||||
|
$on(event, callback) {
|
||||||
|
this._instanceEventEmitter.on(event, callback);
|
||||||
|
|
||||||
|
return () => this._instanceEventEmitter.off(event, callback);
|
||||||
|
}
|
||||||
|
$destroy() {
|
||||||
|
this._observers.forEach(observer => observer());
|
||||||
|
delete this._globalEventEmitter;
|
||||||
|
delete this._observers;
|
||||||
|
this._instanceEventEmitter.emit('$_destroy');
|
||||||
|
}
|
||||||
|
|
||||||
|
static createMutable(object, mutationTopic) {
|
||||||
|
let mutable = Object.create(new MutableDomainObject(mutationTopic));
|
||||||
|
Object.assign(mutable, object);
|
||||||
|
|
||||||
|
mutable.$observe('$_synchronize_model', (updatedObject) => {
|
||||||
|
let clone = JSON.parse(JSON.stringify(updatedObject));
|
||||||
|
let deleted = _.difference(Object.keys(mutable), Object.keys(updatedObject));
|
||||||
|
deleted.forEach((propertyName) => delete mutable[propertyName]);
|
||||||
|
Object.assign(mutable, clone);
|
||||||
|
});
|
||||||
|
|
||||||
|
return mutable;
|
||||||
|
}
|
||||||
|
|
||||||
|
static mutateObject(object, path, value) {
|
||||||
|
_.set(object, path, value);
|
||||||
|
_.set(object, 'modified', Date.now());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function qualifiedEventName(object, eventName) {
|
||||||
|
let keystring = utils.makeKeyString(object.identifier);
|
||||||
|
|
||||||
|
return [keystring, eventName].join(':');
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MutableDomainObject;
|
@ -1,102 +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([
|
|
||||||
'objectUtils',
|
|
||||||
'lodash'
|
|
||||||
], function (
|
|
||||||
utils,
|
|
||||||
_
|
|
||||||
) {
|
|
||||||
const ANY_OBJECT_EVENT = "mutation";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The MutableObject wraps a DomainObject and provides getters and
|
|
||||||
* setters for
|
|
||||||
* @param eventEmitter
|
|
||||||
* @param object
|
|
||||||
* @interface MutableObject
|
|
||||||
*/
|
|
||||||
function MutableObject(eventEmitter, object) {
|
|
||||||
this.eventEmitter = eventEmitter;
|
|
||||||
this.object = object;
|
|
||||||
this.unlisteners = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
function qualifiedEventName(object, eventName) {
|
|
||||||
const keystring = utils.makeKeyString(object.identifier);
|
|
||||||
|
|
||||||
return [keystring, eventName].join(':');
|
|
||||||
}
|
|
||||||
|
|
||||||
MutableObject.prototype.stopListening = function () {
|
|
||||||
this.unlisteners.forEach(function (unlisten) {
|
|
||||||
unlisten();
|
|
||||||
});
|
|
||||||
this.unlisteners = [];
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Observe changes to this domain object.
|
|
||||||
* @param {string} path the property to observe
|
|
||||||
* @param {Function} callback a callback to invoke when new values for
|
|
||||||
* this property are observed
|
|
||||||
* @method on
|
|
||||||
* @memberof module:openmct.MutableObject#
|
|
||||||
*/
|
|
||||||
MutableObject.prototype.on = function (path, callback) {
|
|
||||||
const fullPath = qualifiedEventName(this.object, path);
|
|
||||||
const eventOff =
|
|
||||||
this.eventEmitter.off.bind(this.eventEmitter, fullPath, callback);
|
|
||||||
|
|
||||||
this.eventEmitter.on(fullPath, callback);
|
|
||||||
this.unlisteners.push(eventOff);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Modify this domain object.
|
|
||||||
* @param {string} path the property to modify
|
|
||||||
* @param {*} value the new value for this property
|
|
||||||
* @method set
|
|
||||||
* @memberof module:openmct.MutableObject#
|
|
||||||
*/
|
|
||||||
MutableObject.prototype.set = function (path, value) {
|
|
||||||
_.set(this.object, path, value);
|
|
||||||
_.set(this.object, 'modified', Date.now());
|
|
||||||
|
|
||||||
const handleRecursiveMutation = function (newObject) {
|
|
||||||
this.object = newObject;
|
|
||||||
}.bind(this);
|
|
||||||
|
|
||||||
//Emit wildcard event
|
|
||||||
this.eventEmitter.emit(qualifiedEventName(this.object, '*'), this.object);
|
|
||||||
//Emit a general "any object" event
|
|
||||||
this.eventEmitter.emit(ANY_OBJECT_EVENT, this.object);
|
|
||||||
|
|
||||||
this.eventEmitter.on(qualifiedEventName(this.object, '*'), handleRecursiveMutation);
|
|
||||||
//Emit event specific to property
|
|
||||||
this.eventEmitter.emit(qualifiedEventName(this.object, path), value);
|
|
||||||
this.eventEmitter.off(qualifiedEventName(this.object, '*'), handleRecursiveMutation);
|
|
||||||
};
|
|
||||||
|
|
||||||
return MutableObject;
|
|
||||||
});
|
|
@ -20,323 +20,490 @@
|
|||||||
* at runtime from the About dialog for additional information.
|
* at runtime from the About dialog for additional information.
|
||||||
*****************************************************************************/
|
*****************************************************************************/
|
||||||
|
|
||||||
define([
|
import utils from 'objectUtils';
|
||||||
'lodash',
|
import MutableDomainObject from './MutableDomainObject';
|
||||||
'objectUtils',
|
import RootRegistry from './RootRegistry';
|
||||||
'./MutableObject',
|
import RootObjectProvider from './RootObjectProvider';
|
||||||
'./RootRegistry',
|
import EventEmitter from 'EventEmitter';
|
||||||
'./RootObjectProvider',
|
import InterceptorRegistry from './InterceptorRegistry';
|
||||||
'EventEmitter'
|
|
||||||
], function (
|
|
||||||
_,
|
|
||||||
utils,
|
|
||||||
MutableObject,
|
|
||||||
RootRegistry,
|
|
||||||
RootObjectProvider,
|
|
||||||
EventEmitter
|
|
||||||
) {
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Utilities for loading, saving, and manipulating domain objects.
|
* Utilities for loading, saving, and manipulating domain objects.
|
||||||
* @interface ObjectAPI
|
* @interface ObjectAPI
|
||||||
* @memberof module:openmct
|
* @memberof module:openmct
|
||||||
*/
|
*/
|
||||||
|
|
||||||
function ObjectAPI() {
|
function ObjectAPI(typeRegistry, openmct) {
|
||||||
this.eventEmitter = new EventEmitter();
|
this.typeRegistry = typeRegistry;
|
||||||
this.providers = {};
|
this.eventEmitter = new EventEmitter();
|
||||||
this.rootRegistry = new RootRegistry();
|
this.providers = {};
|
||||||
this.rootProvider = new RootObjectProvider.default(this.rootRegistry);
|
this.rootRegistry = new RootRegistry();
|
||||||
|
this.injectIdentifierService = function () {
|
||||||
|
this.identifierService = openmct.$injector.get("identifierService");
|
||||||
|
};
|
||||||
|
|
||||||
|
this.rootProvider = new RootObjectProvider(this.rootRegistry);
|
||||||
|
this.cache = {};
|
||||||
|
this.interceptorRegistry = new InterceptorRegistry();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set fallback provider, this is an internal API for legacy reasons.
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
ObjectAPI.prototype.supersecretSetFallbackProvider = function (p) {
|
||||||
|
this.fallbackProvider = p;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
ObjectAPI.prototype.getIdentifierService = function () {
|
||||||
|
// Lazily acquire identifier service
|
||||||
|
if (!this.identifierService) {
|
||||||
|
this.injectIdentifierService();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
return this.identifierService;
|
||||||
* Set fallback provider, this is an internal API for legacy reasons.
|
};
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
ObjectAPI.prototype.supersecretSetFallbackProvider = function (p) {
|
|
||||||
this.fallbackProvider = p;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve the provider for a given identifier.
|
* Retrieve the provider for a given identifier.
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
ObjectAPI.prototype.getProvider = function (identifier) {
|
ObjectAPI.prototype.getProvider = function (identifier) {
|
||||||
if (identifier.key === 'ROOT') {
|
//handles the '' vs 'mct' namespace issue
|
||||||
return this.rootProvider;
|
const keyString = utils.makeKeyString(identifier);
|
||||||
}
|
const identifierService = this.getIdentifierService();
|
||||||
|
const namespace = identifierService.parse(keyString).getSpace();
|
||||||
|
|
||||||
return this.providers[identifier.namespace] || this.fallbackProvider;
|
if (identifier.key === 'ROOT') {
|
||||||
};
|
return this.rootProvider;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
return this.providers[namespace] || this.fallbackProvider;
|
||||||
* Get the root-level object.
|
};
|
||||||
* @returns {Promise.<DomainObject>} a promise for the root object
|
|
||||||
*/
|
|
||||||
ObjectAPI.prototype.getRoot = function () {
|
|
||||||
return this.rootProvider.get();
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register a new object provider for a particular namespace.
|
* Get the root-level object.
|
||||||
*
|
* @returns {Promise.<DomainObject>} a promise for the root object
|
||||||
* @param {string} namespace the namespace for which to provide objects
|
*/
|
||||||
* @param {module:openmct.ObjectProvider} provider the provider which
|
ObjectAPI.prototype.getRoot = function () {
|
||||||
* will handle loading domain objects from this namespace
|
return this.rootProvider.get();
|
||||||
* @memberof {module:openmct.ObjectAPI#}
|
};
|
||||||
* @name addProvider
|
|
||||||
*/
|
|
||||||
ObjectAPI.prototype.addProvider = function (namespace, provider) {
|
|
||||||
this.providers[namespace] = provider;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provides the ability to read, write, and delete domain objects.
|
* Register a new object provider for a particular namespace.
|
||||||
*
|
*
|
||||||
* When registering a new object provider, all methods on this interface
|
* @param {string} namespace the namespace for which to provide objects
|
||||||
* are optional.
|
* @param {module:openmct.ObjectProvider} provider the provider which
|
||||||
*
|
* will handle loading domain objects from this namespace
|
||||||
* @interface ObjectProvider
|
* @memberof {module:openmct.ObjectAPI#}
|
||||||
* @memberof module:openmct
|
* @name addProvider
|
||||||
*/
|
*/
|
||||||
|
ObjectAPI.prototype.addProvider = function (namespace, provider) {
|
||||||
|
this.providers[namespace] = provider;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create the given domain object in the corresponding persistence store
|
* Provides the ability to read, write, and delete domain objects.
|
||||||
*
|
*
|
||||||
* @method create
|
* When registering a new object provider, all methods on this interface
|
||||||
* @memberof module:openmct.ObjectProvider#
|
* are optional.
|
||||||
* @param {module:openmct.DomainObject} domainObject the domain object to
|
*
|
||||||
* create
|
* @interface ObjectProvider
|
||||||
* @returns {Promise} a promise which will resolve when the domain object
|
* @memberof module:openmct
|
||||||
* has been created, or be rejected if it cannot be saved
|
*/
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update this domain object in its persistence store
|
* Create the given domain object in the corresponding persistence store
|
||||||
*
|
*
|
||||||
* @method update
|
* @method create
|
||||||
* @memberof module:openmct.ObjectProvider#
|
* @memberof module:openmct.ObjectProvider#
|
||||||
* @param {module:openmct.DomainObject} domainObject the domain object to
|
* @param {module:openmct.DomainObject} domainObject the domain object to
|
||||||
* update
|
* create
|
||||||
* @returns {Promise} a promise which will resolve when the domain object
|
* @returns {Promise} a promise which will resolve when the domain object
|
||||||
* has been updated, or be rejected if it cannot be saved
|
* has been created, or be rejected if it cannot be saved
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete this domain object.
|
* Update this domain object in its persistence store
|
||||||
*
|
*
|
||||||
* @method delete
|
* @method update
|
||||||
* @memberof module:openmct.ObjectProvider#
|
* @memberof module:openmct.ObjectProvider#
|
||||||
* @param {module:openmct.DomainObject} domainObject the domain object to
|
* @param {module:openmct.DomainObject} domainObject the domain object to
|
||||||
* delete
|
* update
|
||||||
* @returns {Promise} a promise which will resolve when the domain object
|
* @returns {Promise} a promise which will resolve when the domain object
|
||||||
* has been deleted, or be rejected if it cannot be deleted
|
* has been updated, or be rejected if it cannot be saved
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a domain object.
|
* Delete this domain object.
|
||||||
*
|
*
|
||||||
* @method get
|
* @method delete
|
||||||
* @memberof module:openmct.ObjectProvider#
|
* @memberof module:openmct.ObjectProvider#
|
||||||
* @param {string} key the key for the domain object to load
|
* @param {module:openmct.DomainObject} domainObject the domain object to
|
||||||
* @returns {Promise} a promise which will resolve when the domain object
|
* delete
|
||||||
* has been saved, or be rejected if it cannot be saved
|
* @returns {Promise} a promise which will resolve when the domain object
|
||||||
*/
|
* has been deleted, or be rejected if it cannot be deleted
|
||||||
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a domain object.
|
* Get a domain object.
|
||||||
*
|
*
|
||||||
* @method get
|
* @method get
|
||||||
* @memberof module:openmct.ObjectAPI#
|
* @memberof module:openmct.ObjectProvider#
|
||||||
* @param {module:openmct.ObjectAPI~Identifier} identifier
|
* @param {string} key the key for the domain object to load
|
||||||
* the identifier for the domain object to load
|
* @returns {Promise} a promise which will resolve when the domain object
|
||||||
* @returns {Promise} a promise which will resolve when the domain object
|
* has been saved, or be rejected if it cannot be saved
|
||||||
* has been saved, or be rejected if it cannot be saved
|
*/
|
||||||
*/
|
|
||||||
ObjectAPI.prototype.get = function (identifier) {
|
|
||||||
identifier = utils.parseKeyString(identifier);
|
|
||||||
const provider = this.getProvider(identifier);
|
|
||||||
|
|
||||||
if (!provider) {
|
ObjectAPI.prototype.get = function (identifier) {
|
||||||
throw new Error('No Provider Matched');
|
let keystring = this.makeKeyString(identifier);
|
||||||
}
|
if (this.cache[keystring] !== undefined) {
|
||||||
|
return this.cache[keystring];
|
||||||
|
}
|
||||||
|
|
||||||
if (!provider.get) {
|
identifier = utils.parseKeyString(identifier);
|
||||||
throw new Error('Provider does not support get!');
|
const provider = this.getProvider(identifier);
|
||||||
}
|
|
||||||
|
|
||||||
return provider.get(identifier);
|
if (!provider) {
|
||||||
};
|
throw new Error('No Provider Matched');
|
||||||
|
}
|
||||||
|
|
||||||
ObjectAPI.prototype.delete = function () {
|
if (!provider.get) {
|
||||||
throw new Error('Delete not implemented');
|
throw new Error('Provider does not support get!');
|
||||||
};
|
}
|
||||||
|
|
||||||
ObjectAPI.prototype.isPersistable = function (domainObject) {
|
let objectPromise = provider.get(identifier);
|
||||||
let provider = this.getProvider(domainObject.identifier);
|
this.cache[keystring] = objectPromise;
|
||||||
|
|
||||||
return provider !== undefined
|
return objectPromise.then(result => {
|
||||||
&& provider.create !== undefined
|
delete this.cache[keystring];
|
||||||
&& provider.update !== undefined;
|
const interceptors = this.listGetInterceptors(identifier, result);
|
||||||
};
|
interceptors.forEach(interceptor => {
|
||||||
|
result = interceptor.invoke(identifier, result);
|
||||||
/**
|
});
|
||||||
* Save this domain object in its current state. EXPERIMENTAL
|
|
||||||
*
|
|
||||||
* @private
|
|
||||||
* @memberof module:openmct.ObjectAPI#
|
|
||||||
* @param {module:openmct.DomainObject} domainObject the domain object to
|
|
||||||
* save
|
|
||||||
* @returns {Promise} a promise which will resolve when the domain object
|
|
||||||
* has been saved, or be rejected if it cannot be saved
|
|
||||||
*/
|
|
||||||
ObjectAPI.prototype.save = function (domainObject) {
|
|
||||||
let provider = this.getProvider(domainObject.identifier);
|
|
||||||
let savedResolve;
|
|
||||||
let result;
|
|
||||||
|
|
||||||
if (!this.isPersistable(domainObject)) {
|
|
||||||
result = Promise.reject('Object provider does not support saving');
|
|
||||||
} else if (hasAlreadyBeenPersisted(domainObject)) {
|
|
||||||
result = Promise.resolve(true);
|
|
||||||
} else {
|
|
||||||
const persistedTime = Date.now();
|
|
||||||
if (domainObject.persisted === undefined) {
|
|
||||||
result = new Promise((resolve) => {
|
|
||||||
savedResolve = resolve;
|
|
||||||
});
|
|
||||||
domainObject.persisted = persistedTime;
|
|
||||||
provider.create(domainObject).then((response) => {
|
|
||||||
this.mutate(domainObject, 'persisted', persistedTime);
|
|
||||||
savedResolve(response);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
domainObject.persisted = persistedTime;
|
|
||||||
this.mutate(domainObject, 'persisted', persistedTime);
|
|
||||||
result = provider.update(domainObject);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
};
|
});
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add a root-level object.
|
* Search for domain objects.
|
||||||
* @param {module:openmct.ObjectAPI~Identifier|function} an array of
|
*
|
||||||
* identifiers for root level objects, or a function that returns a
|
* Object providersSearches and combines results of each object provider search.
|
||||||
* promise for an identifier or an array of root level objects.
|
* Objects without search provided will have been indexed
|
||||||
* @method addRoot
|
* and will be searched using the fallback indexed search.
|
||||||
* @memberof module:openmct.ObjectAPI#
|
* Search results are asynchronous and resolve in parallel.
|
||||||
*/
|
*
|
||||||
ObjectAPI.prototype.addRoot = function (key) {
|
* @method search
|
||||||
this.rootRegistry.addRoot(key);
|
* @memberof module:openmct.ObjectAPI#
|
||||||
};
|
* @param {string} query the term to search for
|
||||||
|
* @param {Object} options search options
|
||||||
|
* @returns {Array.<Promise.<module:openmct.DomainObject>>}
|
||||||
|
* an array of promises returned from each object provider's search function
|
||||||
|
* each resolving to domain objects matching provided search query and options.
|
||||||
|
*/
|
||||||
|
ObjectAPI.prototype.search = function (query, options) {
|
||||||
|
const searchPromises = Object.values(this.providers)
|
||||||
|
.filter(provider => provider.search !== undefined)
|
||||||
|
.map(provider => provider.search(query, options));
|
||||||
|
|
||||||
/**
|
searchPromises.push(this.fallbackProvider.superSecretFallbackSearch(query, options)
|
||||||
* Modify a domain object.
|
.then(results => results.hits
|
||||||
* @param {module:openmct.DomainObject} object the object to mutate
|
.map(hit => utils.toNewFormat(hit.object.getModel(), hit.object.getId()))));
|
||||||
* @param {string} path the property to modify
|
|
||||||
* @param {*} value the new value for this property
|
|
||||||
* @method mutate
|
|
||||||
* @memberof module:openmct.ObjectAPI#
|
|
||||||
*/
|
|
||||||
ObjectAPI.prototype.mutate = function (domainObject, path, value) {
|
|
||||||
const mutableObject =
|
|
||||||
new MutableObject(this.eventEmitter, domainObject);
|
|
||||||
|
|
||||||
return mutableObject.set(path, value);
|
return searchPromises;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Observe changes to a domain object.
|
* Will fetch object for the given identifier, returning a version of the object that will automatically keep
|
||||||
* @param {module:openmct.DomainObject} object the object to observe
|
* itself updated as it is mutated. Before using this function, you should ask yourself whether you really need it.
|
||||||
* @param {string} path the property to observe
|
* The platform will provide mutable objects to views automatically if the underlying object can be mutated. The
|
||||||
* @param {Function} callback a callback to invoke when new values for
|
* platform will manage the lifecycle of any mutable objects that it provides. If you use `getMutable` you are
|
||||||
* this property are observed
|
* committing to managing that lifecycle yourself. `.destroy` should be called when the object is no longer needed.
|
||||||
* @method observe
|
*
|
||||||
* @memberof module:openmct.ObjectAPI#
|
* @memberof {module:openmct.ObjectAPI#}
|
||||||
*/
|
* @returns {Promise.<MutableDomainObject>} a promise that will resolve with a MutableDomainObject if
|
||||||
ObjectAPI.prototype.observe = function (domainObject, path, callback) {
|
* the object can be mutated.
|
||||||
const mutableObject =
|
*/
|
||||||
new MutableObject(this.eventEmitter, domainObject);
|
ObjectAPI.prototype.getMutable = function (idOrKeyString) {
|
||||||
mutableObject.on(path, callback);
|
if (!this.supportsMutation(idOrKeyString)) {
|
||||||
|
throw new Error(`Object "${this.makeKeyString(idOrKeyString)}" does not support mutation.`);
|
||||||
return mutableObject.stopListening.bind(mutableObject);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {module:openmct.ObjectAPI~Identifier} identifier
|
|
||||||
* @returns {string} A string representation of the given identifier, including namespace and key
|
|
||||||
*/
|
|
||||||
ObjectAPI.prototype.makeKeyString = function (identifier) {
|
|
||||||
return utils.makeKeyString(identifier);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Given any number of identifiers, will return true if they are all equal, otherwise false.
|
|
||||||
* @param {module:openmct.ObjectAPI~Identifier[]} identifiers
|
|
||||||
*/
|
|
||||||
ObjectAPI.prototype.areIdsEqual = function (...identifiers) {
|
|
||||||
return identifiers.map(utils.parseKeyString)
|
|
||||||
.every(identifier => {
|
|
||||||
return identifier === identifiers[0]
|
|
||||||
|| (identifier.namespace === identifiers[0].namespace
|
|
||||||
&& identifier.key === identifiers[0].key);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
ObjectAPI.prototype.getOriginalPath = function (identifier, path = []) {
|
|
||||||
return this.get(identifier).then((domainObject) => {
|
|
||||||
path.push(domainObject);
|
|
||||||
let location = domainObject.location;
|
|
||||||
|
|
||||||
if (location) {
|
|
||||||
return this.getOriginalPath(utils.parseKeyString(location), path);
|
|
||||||
} else {
|
|
||||||
return path;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Uniquely identifies a domain object.
|
|
||||||
*
|
|
||||||
* @typedef Identifier
|
|
||||||
* @memberof module:openmct.ObjectAPI~
|
|
||||||
* @property {string} namespace the namespace to/from which this domain
|
|
||||||
* object should be loaded/stored.
|
|
||||||
* @property {string} key a unique identifier for the domain object
|
|
||||||
* within that namespace
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A domain object is an entity of relevance to a user's workflow, that
|
|
||||||
* should appear as a distinct and meaningful object within the user
|
|
||||||
* interface. Examples of domain objects are folders, telemetry sensors,
|
|
||||||
* and so forth.
|
|
||||||
*
|
|
||||||
* A few common properties are defined for domain objects. Beyond these,
|
|
||||||
* individual types of domain objects may add more as they see fit.
|
|
||||||
*
|
|
||||||
* @property {module:openmct.ObjectAPI~Identifier} identifier a key/namespace pair which
|
|
||||||
* uniquely identifies this domain object
|
|
||||||
* @property {string} type the type of domain object
|
|
||||||
* @property {string} name the human-readable name for this domain object
|
|
||||||
* @property {string} [creator] the user name of the creator of this domain
|
|
||||||
* object
|
|
||||||
* @property {number} [modified] the time, in milliseconds since the UNIX
|
|
||||||
* epoch, at which this domain object was last modified
|
|
||||||
* @property {module:openmct.ObjectAPI~Identifier[]} [composition] if
|
|
||||||
* present, this will be used by the default composition provider
|
|
||||||
* to load domain objects
|
|
||||||
* @typedef DomainObject
|
|
||||||
* @memberof module:openmct
|
|
||||||
*/
|
|
||||||
|
|
||||||
function hasAlreadyBeenPersisted(domainObject) {
|
|
||||||
return domainObject.persisted !== undefined
|
|
||||||
&& domainObject.persisted === domainObject.modified;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return ObjectAPI;
|
return this.get(idOrKeyString).then((object) => {
|
||||||
});
|
const mutableDomainObject = this._toMutable(object);
|
||||||
|
|
||||||
|
// Check if provider supports realtime updates
|
||||||
|
let identifier = utils.parseKeyString(idOrKeyString);
|
||||||
|
let provider = this.getProvider(identifier);
|
||||||
|
|
||||||
|
if (provider !== undefined
|
||||||
|
&& provider.observe !== undefined) {
|
||||||
|
let unobserve = provider.observe(identifier, (updatedModel) => {
|
||||||
|
mutableDomainObject.$refresh(updatedModel);
|
||||||
|
});
|
||||||
|
mutableDomainObject.$on('$destroy', () => {
|
||||||
|
unobserve();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return mutableDomainObject;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This function is for cleaning up a mutable domain object when you're done with it.
|
||||||
|
* You only need to use this if you retrieved the object using `getMutable()`. If the object was provided by the
|
||||||
|
* platform (eg. passed into a `view()` function) then the platform is responsible for its lifecycle.
|
||||||
|
* @param {MutableDomainObject} domainObject
|
||||||
|
*/
|
||||||
|
ObjectAPI.prototype.destroyMutable = function (domainObject) {
|
||||||
|
if (domainObject.isMutable) {
|
||||||
|
return domainObject.$destroy();
|
||||||
|
} else {
|
||||||
|
throw new Error("Attempted to destroy non-mutable domain object");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
ObjectAPI.prototype.delete = function () {
|
||||||
|
throw new Error('Delete not implemented');
|
||||||
|
};
|
||||||
|
|
||||||
|
ObjectAPI.prototype.isPersistable = function (idOrKeyString) {
|
||||||
|
let identifier = utils.parseKeyString(idOrKeyString);
|
||||||
|
let provider = this.getProvider(identifier);
|
||||||
|
|
||||||
|
return provider !== undefined
|
||||||
|
&& provider.create !== undefined
|
||||||
|
&& provider.update !== undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save this domain object in its current state. EXPERIMENTAL
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @memberof module:openmct.ObjectAPI#
|
||||||
|
* @param {module:openmct.DomainObject} domainObject the domain object to
|
||||||
|
* save
|
||||||
|
* @returns {Promise} a promise which will resolve when the domain object
|
||||||
|
* has been saved, or be rejected if it cannot be saved
|
||||||
|
*/
|
||||||
|
ObjectAPI.prototype.save = function (domainObject) {
|
||||||
|
let provider = this.getProvider(domainObject.identifier);
|
||||||
|
let savedResolve;
|
||||||
|
let result;
|
||||||
|
|
||||||
|
if (!this.isPersistable(domainObject.identifier)) {
|
||||||
|
result = Promise.reject('Object provider does not support saving');
|
||||||
|
} else if (hasAlreadyBeenPersisted(domainObject)) {
|
||||||
|
result = Promise.resolve(true);
|
||||||
|
} else {
|
||||||
|
const persistedTime = Date.now();
|
||||||
|
if (domainObject.persisted === undefined) {
|
||||||
|
result = new Promise((resolve) => {
|
||||||
|
savedResolve = resolve;
|
||||||
|
});
|
||||||
|
domainObject.persisted = persistedTime;
|
||||||
|
provider.create(domainObject).then((response) => {
|
||||||
|
this.mutate(domainObject, 'persisted', persistedTime);
|
||||||
|
savedResolve(response);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
domainObject.persisted = persistedTime;
|
||||||
|
this.mutate(domainObject, 'persisted', persistedTime);
|
||||||
|
result = provider.update(domainObject);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a root-level object.
|
||||||
|
* @param {module:openmct.ObjectAPI~Identifier|function} an array of
|
||||||
|
* identifiers for root level objects, or a function that returns a
|
||||||
|
* promise for an identifier or an array of root level objects.
|
||||||
|
* @method addRoot
|
||||||
|
* @memberof module:openmct.ObjectAPI#
|
||||||
|
*/
|
||||||
|
ObjectAPI.prototype.addRoot = function (key) {
|
||||||
|
this.rootRegistry.addRoot(key);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register an object interceptor that transforms a domain object requested via module:openmct.ObjectAPI.get
|
||||||
|
* The domain object will be transformed after it is retrieved from the persistence store
|
||||||
|
* The domain object will be transformed only if the interceptor is applicable to that domain object as defined by the InterceptorDef
|
||||||
|
*
|
||||||
|
* @param {module:openmct.InterceptorDef} interceptorDef the interceptor definition to add
|
||||||
|
* @method addGetInterceptor
|
||||||
|
* @memberof module:openmct.InterceptorRegistry#
|
||||||
|
*/
|
||||||
|
ObjectAPI.prototype.addGetInterceptor = function (interceptorDef) {
|
||||||
|
this.interceptorRegistry.addInterceptor(interceptorDef);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the interceptors for a given domain object.
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
ObjectAPI.prototype.listGetInterceptors = function (identifier, object) {
|
||||||
|
return this.interceptorRegistry.getInterceptors(identifier, object);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Modify a domain object.
|
||||||
|
* @param {module:openmct.DomainObject} object the object to mutate
|
||||||
|
* @param {string} path the property to modify
|
||||||
|
* @param {*} value the new value for this property
|
||||||
|
* @method mutate
|
||||||
|
* @memberof module:openmct.ObjectAPI#
|
||||||
|
*/
|
||||||
|
ObjectAPI.prototype.mutate = function (domainObject, path, value) {
|
||||||
|
if (!this.supportsMutation(domainObject.identifier)) {
|
||||||
|
throw `Error: Attempted to mutate immutable object ${domainObject.name}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (domainObject.isMutable) {
|
||||||
|
domainObject.$set(path, value);
|
||||||
|
} else {
|
||||||
|
//Creating a temporary mutable domain object allows other mutable instances of the
|
||||||
|
//object to be kept in sync.
|
||||||
|
let mutableDomainObject = this._toMutable(domainObject);
|
||||||
|
|
||||||
|
//Mutate original object
|
||||||
|
MutableDomainObject.mutateObject(domainObject, path, value);
|
||||||
|
|
||||||
|
//Mutate temporary mutable object, in the process informing any other mutable instances
|
||||||
|
mutableDomainObject.$set(path, value);
|
||||||
|
|
||||||
|
//Destroy temporary mutable object
|
||||||
|
this.destroyMutable(mutableDomainObject);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
ObjectAPI.prototype._toMutable = function (object) {
|
||||||
|
if (object.isMutable) {
|
||||||
|
return object;
|
||||||
|
} else {
|
||||||
|
return MutableDomainObject.createMutable(object, this.eventEmitter);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param module:openmct.ObjectAPI~Identifier identifier An object identifier
|
||||||
|
* @returns {boolean} true if the object can be mutated, otherwise returns false
|
||||||
|
*/
|
||||||
|
ObjectAPI.prototype.supportsMutation = function (identifier) {
|
||||||
|
return this.isPersistable(identifier);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Observe changes to a domain object.
|
||||||
|
* @param {module:openmct.DomainObject} object the object to observe
|
||||||
|
* @param {string} path the property to observe
|
||||||
|
* @param {Function} callback a callback to invoke when new values for
|
||||||
|
* this property are observed
|
||||||
|
* @method observe
|
||||||
|
* @memberof module:openmct.ObjectAPI#
|
||||||
|
*/
|
||||||
|
ObjectAPI.prototype.observe = function (domainObject, path, callback) {
|
||||||
|
if (domainObject.isMutable) {
|
||||||
|
return domainObject.$observe(path, callback);
|
||||||
|
} else {
|
||||||
|
let mutable = this._toMutable(domainObject);
|
||||||
|
mutable.$observe(path, callback);
|
||||||
|
|
||||||
|
return () => mutable.$destroy();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {module:openmct.ObjectAPI~Identifier} identifier
|
||||||
|
* @returns {string} A string representation of the given identifier, including namespace and key
|
||||||
|
*/
|
||||||
|
ObjectAPI.prototype.makeKeyString = function (identifier) {
|
||||||
|
return utils.makeKeyString(identifier);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} keyString A string representation of the given identifier, that is, a namespace and key separated by a colon.
|
||||||
|
* @returns {module:openmct.ObjectAPI~Identifier} An identifier object
|
||||||
|
*/
|
||||||
|
ObjectAPI.prototype.parseKeyString = function (keyString) {
|
||||||
|
return utils.parseKeyString(keyString);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given any number of identifiers, will return true if they are all equal, otherwise false.
|
||||||
|
* @param {module:openmct.ObjectAPI~Identifier[]} identifiers
|
||||||
|
*/
|
||||||
|
ObjectAPI.prototype.areIdsEqual = function (...identifiers) {
|
||||||
|
return identifiers.map(utils.parseKeyString)
|
||||||
|
.every(identifier => {
|
||||||
|
return identifier === identifiers[0]
|
||||||
|
|| (identifier.namespace === identifiers[0].namespace
|
||||||
|
&& identifier.key === identifiers[0].key);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
ObjectAPI.prototype.getOriginalPath = function (identifier, path = []) {
|
||||||
|
return this.get(identifier).then((domainObject) => {
|
||||||
|
path.push(domainObject);
|
||||||
|
let location = domainObject.location;
|
||||||
|
|
||||||
|
if (location) {
|
||||||
|
return this.getOriginalPath(utils.parseKeyString(location), path);
|
||||||
|
} else {
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Uniquely identifies a domain object.
|
||||||
|
*
|
||||||
|
* @typedef Identifier
|
||||||
|
* @memberof module:openmct.ObjectAPI~
|
||||||
|
* @property {string} namespace the namespace to/from which this domain
|
||||||
|
* object should be loaded/stored.
|
||||||
|
* @property {string} key a unique identifier for the domain object
|
||||||
|
* within that namespace
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A domain object is an entity of relevance to a user's workflow, that
|
||||||
|
* should appear as a distinct and meaningful object within the user
|
||||||
|
* interface. Examples of domain objects are folders, telemetry sensors,
|
||||||
|
* and so forth.
|
||||||
|
*
|
||||||
|
* A few common properties are defined for domain objects. Beyond these,
|
||||||
|
* individual types of domain objects may add more as they see fit.
|
||||||
|
*
|
||||||
|
* @property {module:openmct.ObjectAPI~Identifier} identifier a key/namespace pair which
|
||||||
|
* uniquely identifies this domain object
|
||||||
|
* @property {string} type the type of domain object
|
||||||
|
* @property {string} name the human-readable name for this domain object
|
||||||
|
* @property {string} [creator] the user name of the creator of this domain
|
||||||
|
* object
|
||||||
|
* @property {number} [modified] the time, in milliseconds since the UNIX
|
||||||
|
* epoch, at which this domain object was last modified
|
||||||
|
* @property {module:openmct.ObjectAPI~Identifier[]} [composition] if
|
||||||
|
* present, this will be used by the default composition provider
|
||||||
|
* to load domain objects
|
||||||
|
* @typedef DomainObject
|
||||||
|
* @memberof module:openmct
|
||||||
|
*/
|
||||||
|
|
||||||
|
function hasAlreadyBeenPersisted(domainObject) {
|
||||||
|
return domainObject.persisted !== undefined
|
||||||
|
&& domainObject.persisted === domainObject.modified;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ObjectAPI;
|
||||||
|
119
src/api/objects/ObjectAPISearchSpec.js
Normal file
119
src/api/objects/ObjectAPISearchSpec.js
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
import ObjectAPI from './ObjectAPI.js';
|
||||||
|
|
||||||
|
describe("The Object API Search Function", () => {
|
||||||
|
const MOCK_PROVIDER_KEY = 'mockProvider';
|
||||||
|
const ANOTHER_MOCK_PROVIDER_KEY = 'anotherMockProvider';
|
||||||
|
const MOCK_PROVIDER_SEARCH_DELAY = 15000;
|
||||||
|
const ANOTHER_MOCK_PROVIDER_SEARCH_DELAY = 20000;
|
||||||
|
const TOTAL_TIME_ELAPSED = 21000;
|
||||||
|
const BASE_TIME = new Date(2021, 0, 1);
|
||||||
|
|
||||||
|
let objectAPI;
|
||||||
|
let mockObjectProvider;
|
||||||
|
let anotherMockObjectProvider;
|
||||||
|
let mockFallbackProvider;
|
||||||
|
let fallbackProviderSearchResults;
|
||||||
|
let resultsPromises;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jasmine.clock().install();
|
||||||
|
jasmine.clock().mockDate(BASE_TIME);
|
||||||
|
|
||||||
|
resultsPromises = [];
|
||||||
|
fallbackProviderSearchResults = {
|
||||||
|
hits: []
|
||||||
|
};
|
||||||
|
|
||||||
|
objectAPI = new ObjectAPI();
|
||||||
|
|
||||||
|
mockObjectProvider = jasmine.createSpyObj("mock object provider", [
|
||||||
|
"search"
|
||||||
|
]);
|
||||||
|
anotherMockObjectProvider = jasmine.createSpyObj("another mock object provider", [
|
||||||
|
"search"
|
||||||
|
]);
|
||||||
|
mockFallbackProvider = jasmine.createSpyObj("super secret fallback provider", [
|
||||||
|
"superSecretFallbackSearch"
|
||||||
|
]);
|
||||||
|
objectAPI.addProvider('objects', mockObjectProvider);
|
||||||
|
objectAPI.addProvider('other-objects', anotherMockObjectProvider);
|
||||||
|
objectAPI.supersecretSetFallbackProvider(mockFallbackProvider);
|
||||||
|
|
||||||
|
mockObjectProvider.search.and.callFake(() => {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
const mockProviderSearch = {
|
||||||
|
name: MOCK_PROVIDER_KEY,
|
||||||
|
start: new Date()
|
||||||
|
};
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
mockProviderSearch.end = new Date();
|
||||||
|
|
||||||
|
return resolve(mockProviderSearch);
|
||||||
|
}, MOCK_PROVIDER_SEARCH_DELAY);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
anotherMockObjectProvider.search.and.callFake(() => {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
const anotherMockProviderSearch = {
|
||||||
|
name: ANOTHER_MOCK_PROVIDER_KEY,
|
||||||
|
start: new Date()
|
||||||
|
};
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
anotherMockProviderSearch.end = new Date();
|
||||||
|
|
||||||
|
return resolve(anotherMockProviderSearch);
|
||||||
|
}, ANOTHER_MOCK_PROVIDER_SEARCH_DELAY);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
mockFallbackProvider.superSecretFallbackSearch.and.callFake(
|
||||||
|
() => new Promise(
|
||||||
|
resolve => setTimeout(
|
||||||
|
() => resolve(fallbackProviderSearchResults),
|
||||||
|
50
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
resultsPromises = objectAPI.search('foo');
|
||||||
|
|
||||||
|
jasmine.clock().tick(TOTAL_TIME_ELAPSED);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jasmine.clock().uninstall();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("uses each objects given provider's search function", () => {
|
||||||
|
expect(mockObjectProvider.search).toHaveBeenCalled();
|
||||||
|
expect(anotherMockObjectProvider.search).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("uses the fallback indexed search for objects without a search function provided", () => {
|
||||||
|
expect(mockFallbackProvider.superSecretFallbackSearch).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("provides each providers results as promises that resolve in parallel", async () => {
|
||||||
|
const results = await Promise.all(resultsPromises);
|
||||||
|
const mockProviderResults = results.find(
|
||||||
|
result => result.name === MOCK_PROVIDER_KEY
|
||||||
|
);
|
||||||
|
const anotherMockProviderResults = results.find(
|
||||||
|
result => result.name === ANOTHER_MOCK_PROVIDER_KEY
|
||||||
|
);
|
||||||
|
const mockProviderStart = mockProviderResults.start.getTime();
|
||||||
|
const mockProviderEnd = mockProviderResults.end.getTime();
|
||||||
|
const anotherMockProviderStart = anotherMockProviderResults.start.getTime();
|
||||||
|
const anotherMockProviderEnd = anotherMockProviderResults.end.getTime();
|
||||||
|
const searchElapsedTime = Math.max(mockProviderEnd, anotherMockProviderEnd)
|
||||||
|
- Math.min(mockProviderEnd, anotherMockProviderEnd);
|
||||||
|
|
||||||
|
expect(mockProviderStart).toBeLessThan(anotherMockProviderEnd);
|
||||||
|
expect(anotherMockProviderStart).toBeLessThan(mockProviderEnd);
|
||||||
|
expect(searchElapsedTime).toBeLessThan(
|
||||||
|
MOCK_PROVIDER_SEARCH_DELAY
|
||||||
|
+ ANOTHER_MOCK_PROVIDER_SEARCH_DELAY
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
@ -2,12 +2,30 @@ import ObjectAPI from './ObjectAPI.js';
|
|||||||
|
|
||||||
describe("The Object API", () => {
|
describe("The Object API", () => {
|
||||||
let objectAPI;
|
let objectAPI;
|
||||||
|
let typeRegistry;
|
||||||
|
let openmct = {};
|
||||||
|
let mockIdentifierService;
|
||||||
let mockDomainObject;
|
let mockDomainObject;
|
||||||
const TEST_NAMESPACE = "test-namespace";
|
const TEST_NAMESPACE = "test-namespace";
|
||||||
const FIFTEEN_MINUTES = 15 * 60 * 1000;
|
const FIFTEEN_MINUTES = 15 * 60 * 1000;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
objectAPI = new ObjectAPI();
|
typeRegistry = jasmine.createSpyObj('typeRegistry', [
|
||||||
|
'get'
|
||||||
|
]);
|
||||||
|
openmct.$injector = jasmine.createSpyObj('$injector', ['get']);
|
||||||
|
mockIdentifierService = jasmine.createSpyObj(
|
||||||
|
'identifierService',
|
||||||
|
['parse']
|
||||||
|
);
|
||||||
|
mockIdentifierService.parse.and.returnValue({
|
||||||
|
getSpace: () => {
|
||||||
|
return TEST_NAMESPACE;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
openmct.$injector.get.and.returnValue(mockIdentifierService);
|
||||||
|
objectAPI = new ObjectAPI(typeRegistry, openmct);
|
||||||
mockDomainObject = {
|
mockDomainObject = {
|
||||||
identifier: {
|
identifier: {
|
||||||
namespace: TEST_NAMESPACE,
|
namespace: TEST_NAMESPACE,
|
||||||
@ -33,6 +51,7 @@ describe("The Object API", () => {
|
|||||||
"update"
|
"update"
|
||||||
]);
|
]);
|
||||||
mockProvider.create.and.returnValue(Promise.resolve(true));
|
mockProvider.create.and.returnValue(Promise.resolve(true));
|
||||||
|
mockProvider.update.and.returnValue(Promise.resolve(true));
|
||||||
objectAPI.addProvider(TEST_NAMESPACE, mockProvider);
|
objectAPI.addProvider(TEST_NAMESPACE, mockProvider);
|
||||||
});
|
});
|
||||||
it("Calls 'create' on provider if object is new", () => {
|
it("Calls 'create' on provider if object is new", () => {
|
||||||
@ -59,4 +78,224 @@ describe("The Object API", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("The get function", () => {
|
||||||
|
describe("when a provider is available", () => {
|
||||||
|
let mockProvider;
|
||||||
|
let mockInterceptor;
|
||||||
|
let anotherMockInterceptor;
|
||||||
|
let notApplicableMockInterceptor;
|
||||||
|
beforeEach(() => {
|
||||||
|
mockProvider = jasmine.createSpyObj("mock provider", [
|
||||||
|
"get"
|
||||||
|
]);
|
||||||
|
mockProvider.get.and.returnValue(Promise.resolve(mockDomainObject));
|
||||||
|
|
||||||
|
mockInterceptor = jasmine.createSpyObj("mock interceptor", [
|
||||||
|
"appliesTo",
|
||||||
|
"invoke"
|
||||||
|
]);
|
||||||
|
mockInterceptor.appliesTo.and.returnValue(true);
|
||||||
|
mockInterceptor.invoke.and.callFake((identifier, object) => {
|
||||||
|
return Object.assign({
|
||||||
|
changed: true
|
||||||
|
}, object);
|
||||||
|
});
|
||||||
|
|
||||||
|
anotherMockInterceptor = jasmine.createSpyObj("another mock interceptor", [
|
||||||
|
"appliesTo",
|
||||||
|
"invoke"
|
||||||
|
]);
|
||||||
|
anotherMockInterceptor.appliesTo.and.returnValue(true);
|
||||||
|
anotherMockInterceptor.invoke.and.callFake((identifier, object) => {
|
||||||
|
return Object.assign({
|
||||||
|
alsoChanged: true
|
||||||
|
}, object);
|
||||||
|
});
|
||||||
|
|
||||||
|
notApplicableMockInterceptor = jasmine.createSpyObj("not applicable mock interceptor", [
|
||||||
|
"appliesTo",
|
||||||
|
"invoke"
|
||||||
|
]);
|
||||||
|
notApplicableMockInterceptor.appliesTo.and.returnValue(false);
|
||||||
|
notApplicableMockInterceptor.invoke.and.callFake((identifier, object) => {
|
||||||
|
return Object.assign({
|
||||||
|
shouldNotBeChanged: true
|
||||||
|
}, object);
|
||||||
|
});
|
||||||
|
objectAPI.addProvider(TEST_NAMESPACE, mockProvider);
|
||||||
|
objectAPI.addGetInterceptor(mockInterceptor);
|
||||||
|
objectAPI.addGetInterceptor(anotherMockInterceptor);
|
||||||
|
objectAPI.addGetInterceptor(notApplicableMockInterceptor);
|
||||||
|
});
|
||||||
|
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("applies any applicable interceptors", () => {
|
||||||
|
expect(mockDomainObject.changed).toBeUndefined();
|
||||||
|
objectAPI.get(mockDomainObject.identifier).then((object) => {
|
||||||
|
expect(object.changed).toBeTrue();
|
||||||
|
expect(object.alsoChanged).toBeTrue();
|
||||||
|
expect(object.shouldNotBeChanged).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("the mutation API", () => {
|
||||||
|
let testObject;
|
||||||
|
let updatedTestObject;
|
||||||
|
let mutable;
|
||||||
|
let mockProvider;
|
||||||
|
let callbacks = [];
|
||||||
|
|
||||||
|
beforeEach(function () {
|
||||||
|
objectAPI = new ObjectAPI(typeRegistry, openmct);
|
||||||
|
testObject = {
|
||||||
|
identifier: {
|
||||||
|
namespace: TEST_NAMESPACE,
|
||||||
|
key: 'test-key'
|
||||||
|
},
|
||||||
|
name: 'test object',
|
||||||
|
otherAttribute: 'other-attribute-value',
|
||||||
|
objectAttribute: {
|
||||||
|
embeddedObject: {
|
||||||
|
embeddedKey: 'embedded-value'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
updatedTestObject = Object.assign({otherAttribute: 'changed-attribute-value'}, testObject);
|
||||||
|
mockProvider = jasmine.createSpyObj("mock provider", [
|
||||||
|
"get",
|
||||||
|
"create",
|
||||||
|
"update",
|
||||||
|
"observe",
|
||||||
|
"observeObjectChanges"
|
||||||
|
]);
|
||||||
|
mockProvider.get.and.returnValue(Promise.resolve(testObject));
|
||||||
|
mockProvider.observeObjectChanges.and.callFake(() => {
|
||||||
|
callbacks[0](updatedTestObject);
|
||||||
|
callbacks.splice(0, 1);
|
||||||
|
});
|
||||||
|
mockProvider.observe.and.callFake((id, callback) => {
|
||||||
|
if (callbacks.length === 0) {
|
||||||
|
callbacks.push(callback);
|
||||||
|
} else {
|
||||||
|
callbacks[0] = callback;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
objectAPI.addProvider(TEST_NAMESPACE, mockProvider);
|
||||||
|
|
||||||
|
return objectAPI.getMutable(testObject.identifier)
|
||||||
|
.then(object => {
|
||||||
|
mutable = object;
|
||||||
|
|
||||||
|
return mutable;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
mutable.$destroy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('mutates the original object', () => {
|
||||||
|
const MUTATED_NAME = 'mutated name';
|
||||||
|
objectAPI.mutate(testObject, 'name', MUTATED_NAME);
|
||||||
|
expect(testObject.name).toBe(MUTATED_NAME);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe ('uses a MutableDomainObject', () => {
|
||||||
|
it('and retains properties of original object ', function () {
|
||||||
|
expect(hasOwnProperty(mutable, 'identifier')).toBe(true);
|
||||||
|
expect(hasOwnProperty(mutable, 'otherAttribute')).toBe(true);
|
||||||
|
expect(mutable.identifier).toEqual(testObject.identifier);
|
||||||
|
expect(mutable.otherAttribute).toEqual(testObject.otherAttribute);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('that is identical to original object when serialized', function () {
|
||||||
|
expect(JSON.stringify(mutable)).toEqual(JSON.stringify(testObject));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('that observes for object changes', function () {
|
||||||
|
let mockListener = jasmine.createSpy('mockListener');
|
||||||
|
objectAPI.observe(testObject, '*', mockListener);
|
||||||
|
mockProvider.observeObjectChanges();
|
||||||
|
expect(mockListener).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('uses events', function () {
|
||||||
|
let testObjectDuplicate;
|
||||||
|
let mutableSecondInstance;
|
||||||
|
|
||||||
|
beforeEach(function () {
|
||||||
|
// Duplicate object to guarantee we are not sharing object instance, which would invalidate test
|
||||||
|
testObjectDuplicate = JSON.parse(JSON.stringify(testObject));
|
||||||
|
mutableSecondInstance = objectAPI._toMutable(testObjectDuplicate);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
mutableSecondInstance.$destroy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('to stay synchronized when mutated', function () {
|
||||||
|
objectAPI.mutate(mutable, 'otherAttribute', 'new-attribute-value');
|
||||||
|
expect(mutableSecondInstance.otherAttribute).toBe('new-attribute-value');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('to indicate when a property changes', function () {
|
||||||
|
let mutationCallback = jasmine.createSpy('mutation-callback');
|
||||||
|
let unlisten;
|
||||||
|
|
||||||
|
return new Promise(function (resolve) {
|
||||||
|
mutationCallback.and.callFake(resolve);
|
||||||
|
unlisten = objectAPI.observe(mutableSecondInstance, 'otherAttribute', mutationCallback);
|
||||||
|
objectAPI.mutate(mutable, 'otherAttribute', 'some-new-value');
|
||||||
|
}).then(function () {
|
||||||
|
expect(mutationCallback).toHaveBeenCalledWith('some-new-value');
|
||||||
|
unlisten();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('to indicate when a child property has changed', function () {
|
||||||
|
let embeddedKeyCallback = jasmine.createSpy('embeddedKeyCallback');
|
||||||
|
let embeddedObjectCallback = jasmine.createSpy('embeddedObjectCallback');
|
||||||
|
let objectAttributeCallback = jasmine.createSpy('objectAttribute');
|
||||||
|
let listeners = [];
|
||||||
|
|
||||||
|
return new Promise(function (resolve) {
|
||||||
|
objectAttributeCallback.and.callFake(resolve);
|
||||||
|
|
||||||
|
listeners.push(objectAPI.observe(mutableSecondInstance, 'objectAttribute.embeddedObject.embeddedKey', embeddedKeyCallback));
|
||||||
|
listeners.push(objectAPI.observe(mutableSecondInstance, 'objectAttribute.embeddedObject', embeddedObjectCallback));
|
||||||
|
listeners.push(objectAPI.observe(mutableSecondInstance, 'objectAttribute', objectAttributeCallback));
|
||||||
|
|
||||||
|
objectAPI.mutate(mutable, 'objectAttribute.embeddedObject.embeddedKey', 'updated-embedded-value');
|
||||||
|
}).then(function () {
|
||||||
|
expect(embeddedKeyCallback).toHaveBeenCalledWith('updated-embedded-value');
|
||||||
|
expect(embeddedObjectCallback).toHaveBeenCalledWith({
|
||||||
|
embeddedKey: 'updated-embedded-value'
|
||||||
|
});
|
||||||
|
expect(objectAttributeCallback).toHaveBeenCalledWith({
|
||||||
|
embeddedObject: {
|
||||||
|
embeddedKey: 'updated-embedded-value'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
listeners.forEach(listener => listener());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function hasOwnProperty(object, property) {
|
||||||
|
return Object.prototype.hasOwnProperty.call(object, property);
|
||||||
|
}
|
||||||
|
@ -48,7 +48,7 @@ define([
|
|||||||
this.providers.push(function () {
|
this.providers.push(function () {
|
||||||
return key;
|
return key;
|
||||||
});
|
});
|
||||||
} else if (_.isFunction(key)) {
|
} else if (typeof key === "function") {
|
||||||
this.providers.push(key);
|
this.providers.push(key);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -6,6 +6,9 @@ class Dialog extends Overlay {
|
|||||||
constructor({iconClass, message, title, hint, timestamp, ...options}) {
|
constructor({iconClass, message, title, hint, timestamp, ...options}) {
|
||||||
|
|
||||||
let component = new Vue({
|
let component = new Vue({
|
||||||
|
components: {
|
||||||
|
DialogComponent: DialogComponent
|
||||||
|
},
|
||||||
provide: {
|
provide: {
|
||||||
iconClass,
|
iconClass,
|
||||||
message,
|
message,
|
||||||
@ -13,9 +16,6 @@ class Dialog extends Overlay {
|
|||||||
hint,
|
hint,
|
||||||
timestamp
|
timestamp
|
||||||
},
|
},
|
||||||
components: {
|
|
||||||
DialogComponent: DialogComponent
|
|
||||||
},
|
|
||||||
template: '<dialog-component></dialog-component>'
|
template: '<dialog-component></dialog-component>'
|
||||||
}).$mount();
|
}).$mount();
|
||||||
|
|
||||||
|
@ -22,6 +22,7 @@ class OverlayAPI {
|
|||||||
this.dismissLastOverlay();
|
this.dismissLastOverlay();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -127,6 +128,7 @@ class OverlayAPI {
|
|||||||
|
|
||||||
return progressDialog;
|
return progressDialog;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default OverlayAPI;
|
export default OverlayAPI;
|
||||||
|
@ -7,6 +7,9 @@ let component;
|
|||||||
class ProgressDialog extends Overlay {
|
class ProgressDialog extends Overlay {
|
||||||
constructor({progressPerc, progressText, iconClass, message, title, hint, timestamp, ...options}) {
|
constructor({progressPerc, progressText, iconClass, message, title, hint, timestamp, ...options}) {
|
||||||
component = new Vue({
|
component = new Vue({
|
||||||
|
components: {
|
||||||
|
ProgressDialogComponent: ProgressDialogComponent
|
||||||
|
},
|
||||||
provide: {
|
provide: {
|
||||||
iconClass,
|
iconClass,
|
||||||
message,
|
message,
|
||||||
@ -14,9 +17,6 @@ class ProgressDialog extends Overlay {
|
|||||||
hint,
|
hint,
|
||||||
timestamp
|
timestamp
|
||||||
},
|
},
|
||||||
components: {
|
|
||||||
ProgressDialogComponent: ProgressDialogComponent
|
|
||||||
},
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
model: {
|
model: {
|
||||||
|
@ -38,12 +38,12 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
|
inject: ['dismiss', 'element', 'buttons', 'dismissable'],
|
||||||
data: function () {
|
data: function () {
|
||||||
return {
|
return {
|
||||||
focusIndex: -1
|
focusIndex: -1
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
inject: ['dismiss', 'element', 'buttons', 'dismissable'],
|
|
||||||
mounted() {
|
mounted() {
|
||||||
const element = this.$refs.element;
|
const element = this.$refs.element;
|
||||||
element.appendChild(this.element);
|
element.appendChild(this.element);
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -21,12 +21,14 @@
|
|||||||
*****************************************************************************/
|
*****************************************************************************/
|
||||||
|
|
||||||
define([
|
define([
|
||||||
|
'../../plugins/displayLayout/CustomStringFormatter',
|
||||||
'./TelemetryMetadataManager',
|
'./TelemetryMetadataManager',
|
||||||
'./TelemetryValueFormatter',
|
'./TelemetryValueFormatter',
|
||||||
'./DefaultMetadataProvider',
|
'./DefaultMetadataProvider',
|
||||||
'objectUtils',
|
'objectUtils',
|
||||||
'lodash'
|
'lodash'
|
||||||
], function (
|
], function (
|
||||||
|
CustomStringFormatter,
|
||||||
TelemetryMetadataManager,
|
TelemetryMetadataManager,
|
||||||
TelemetryValueFormatter,
|
TelemetryValueFormatter,
|
||||||
DefaultMetadataProvider,
|
DefaultMetadataProvider,
|
||||||
@ -142,6 +144,17 @@ define([
|
|||||||
this.valueFormatterCache = new WeakMap();
|
this.valueFormatterCache = new WeakMap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return Custom String Formatter
|
||||||
|
*
|
||||||
|
* @param {Object} valueMetadata valueMetadata for given telemetry object
|
||||||
|
* @param {string} format custom formatter string (eg: %.4f, <s etc.)
|
||||||
|
* @returns {CustomStringFormatter}
|
||||||
|
*/
|
||||||
|
TelemetryAPI.prototype.customStringFormatter = function (valueMetadata, format) {
|
||||||
|
return new CustomStringFormatter.default(this.openmct, valueMetadata, format);
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return true if the given domainObject is a telemetry object. A telemetry
|
* Return true if the given domainObject is a telemetry object. A telemetry
|
||||||
* object is any object which has telemetry metadata-- regardless of whether
|
* object is any object which has telemetry metadata-- regardless of whether
|
||||||
@ -400,6 +413,17 @@ define([
|
|||||||
return _.sortBy(options, sortKeys);
|
return _.sortBy(options, sortKeys);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
TelemetryAPI.prototype.getFormatService = function () {
|
||||||
|
if (!this.formatService) {
|
||||||
|
this.formatService = this.openmct.$injector.get('formatService');
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.formatService;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a value formatter for a given valueMetadata.
|
* Get a value formatter for a given valueMetadata.
|
||||||
*
|
*
|
||||||
@ -407,19 +431,27 @@ define([
|
|||||||
*/
|
*/
|
||||||
TelemetryAPI.prototype.getValueFormatter = function (valueMetadata) {
|
TelemetryAPI.prototype.getValueFormatter = function (valueMetadata) {
|
||||||
if (!this.valueFormatterCache.has(valueMetadata)) {
|
if (!this.valueFormatterCache.has(valueMetadata)) {
|
||||||
if (!this.formatService) {
|
|
||||||
this.formatService = this.openmct.$injector.get('formatService');
|
|
||||||
}
|
|
||||||
|
|
||||||
this.valueFormatterCache.set(
|
this.valueFormatterCache.set(
|
||||||
valueMetadata,
|
valueMetadata,
|
||||||
new TelemetryValueFormatter(valueMetadata, this.formatService)
|
new TelemetryValueFormatter(valueMetadata, this.getFormatService())
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.valueFormatterCache.get(valueMetadata);
|
return this.valueFormatterCache.get(valueMetadata);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a value formatter for a given key.
|
||||||
|
* @param {string} key
|
||||||
|
*
|
||||||
|
* @returns {Format}
|
||||||
|
*/
|
||||||
|
TelemetryAPI.prototype.getFormatter = function (key) {
|
||||||
|
const formatMap = this.getFormatService().formatMap;
|
||||||
|
|
||||||
|
return formatMap[key];
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a format map of all value formatters for a given piece of telemetry
|
* Get a format map of all value formatters for a given piece of telemetry
|
||||||
* metadata.
|
* metadata.
|
||||||
|
37
src/plugins/CouchDBSearchFolder/plugin.js
Normal file
37
src/plugins/CouchDBSearchFolder/plugin.js
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
export default function (couchPlugin, searchFilter) {
|
||||||
|
return function install(openmct) {
|
||||||
|
const couchProvider = couchPlugin.couchProvider;
|
||||||
|
|
||||||
|
openmct.objects.addRoot({
|
||||||
|
namespace: 'couch-search',
|
||||||
|
key: 'couch-search'
|
||||||
|
});
|
||||||
|
|
||||||
|
openmct.objects.addProvider('couch-search', {
|
||||||
|
get(identifier) {
|
||||||
|
if (identifier.key !== 'couch-search') {
|
||||||
|
return undefined;
|
||||||
|
} else {
|
||||||
|
return Promise.resolve({
|
||||||
|
identifier,
|
||||||
|
type: 'folder',
|
||||||
|
name: "CouchDB Documents"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
openmct.composition.addProvider({
|
||||||
|
appliesTo(domainObject) {
|
||||||
|
return domainObject.identifier.namespace === 'couch-search'
|
||||||
|
&& domainObject.identifier.key === 'couch-search';
|
||||||
|
},
|
||||||
|
load() {
|
||||||
|
return couchProvider.getObjectsByFilter(searchFilter).then(objects => {
|
||||||
|
return objects.map(object => object.identifier);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
91
src/plugins/CouchDBSearchFolder/pluginSpec.js
Normal file
91
src/plugins/CouchDBSearchFolder/pluginSpec.js
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
/*****************************************************************************
|
||||||
|
* Open MCT, Copyright (c) 2014-2020, United States Government
|
||||||
|
* as represented by the Administrator of the National Aeronautics and Space
|
||||||
|
* Administration. All rights reserved.
|
||||||
|
*
|
||||||
|
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
* License for the specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*
|
||||||
|
* Open MCT includes source code licensed under additional open source
|
||||||
|
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||||
|
* this source code distribution or the Licensing information page available
|
||||||
|
* at runtime from the About dialog for additional information.
|
||||||
|
*****************************************************************************/
|
||||||
|
|
||||||
|
import {createOpenMct, resetApplicationState} from "utils/testing";
|
||||||
|
import CouchDBSearchFolderPlugin from './plugin';
|
||||||
|
|
||||||
|
describe('the plugin', function () {
|
||||||
|
let identifier = {
|
||||||
|
namespace: 'couch-search',
|
||||||
|
key: "couch-search"
|
||||||
|
};
|
||||||
|
let testPath = '/test/db';
|
||||||
|
let openmct;
|
||||||
|
let composition;
|
||||||
|
|
||||||
|
beforeEach((done) => {
|
||||||
|
|
||||||
|
openmct = createOpenMct();
|
||||||
|
|
||||||
|
let couchPlugin = openmct.plugins.CouchDB(testPath);
|
||||||
|
openmct.install(couchPlugin);
|
||||||
|
|
||||||
|
openmct.install(new CouchDBSearchFolderPlugin(couchPlugin, {
|
||||||
|
"selector": {
|
||||||
|
"model": {
|
||||||
|
"type": "plan"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
openmct.on('start', done);
|
||||||
|
openmct.startHeadless();
|
||||||
|
|
||||||
|
composition = openmct.composition.get({identifier});
|
||||||
|
|
||||||
|
spyOn(couchPlugin.couchProvider, 'getObjectsByFilter').and.returnValue(Promise.resolve([
|
||||||
|
{
|
||||||
|
identifier: {
|
||||||
|
key: "1",
|
||||||
|
namespace: "mct"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
identifier: {
|
||||||
|
key: "2",
|
||||||
|
namespace: "mct"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]));
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
return resetApplicationState(openmct);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('provides a folder to hold plans', () => {
|
||||||
|
openmct.objects.get(identifier).then((object) => {
|
||||||
|
expect(object).toEqual({
|
||||||
|
identifier,
|
||||||
|
type: 'folder',
|
||||||
|
name: "CouchDB Documents"
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('provides composition for couch search folders', () => {
|
||||||
|
composition.load().then((objects) => {
|
||||||
|
expect(objects.length).toEqual(2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
@ -44,11 +44,15 @@ export default function LADTableViewProvider(openmct) {
|
|||||||
LadTableComponent: LadTable
|
LadTableComponent: LadTable
|
||||||
},
|
},
|
||||||
provide: {
|
provide: {
|
||||||
openmct,
|
openmct
|
||||||
domainObject,
|
|
||||||
objectPath
|
|
||||||
},
|
},
|
||||||
template: '<lad-table-component></lad-table-component>'
|
data: () => {
|
||||||
|
return {
|
||||||
|
domainObject,
|
||||||
|
objectPath
|
||||||
|
};
|
||||||
|
},
|
||||||
|
template: '<lad-table-component :domain-object="domainObject" :object-path="objectPath"></lad-table-component>'
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
destroy: function (element) {
|
destroy: function (element) {
|
||||||
|
@ -26,7 +26,7 @@
|
|||||||
class="js-lad-table__body__row"
|
class="js-lad-table__body__row"
|
||||||
@contextmenu.prevent="showContextMenu"
|
@contextmenu.prevent="showContextMenu"
|
||||||
>
|
>
|
||||||
<td class="js-first-data">{{ name }}</td>
|
<td class="js-first-data">{{ domainObject.name }}</td>
|
||||||
<td class="js-second-data">{{ formattedTimestamp }}</td>
|
<td class="js-second-data">{{ formattedTimestamp }}</td>
|
||||||
<td
|
<td
|
||||||
class="js-third-data"
|
class="js-third-data"
|
||||||
@ -44,17 +44,22 @@
|
|||||||
<script>
|
<script>
|
||||||
|
|
||||||
const CONTEXT_MENU_ACTIONS = [
|
const CONTEXT_MENU_ACTIONS = [
|
||||||
|
'viewDatumAction',
|
||||||
'viewHistoricalData',
|
'viewHistoricalData',
|
||||||
'remove'
|
'remove'
|
||||||
];
|
];
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
inject: ['openmct', 'objectPath'],
|
inject: ['openmct'],
|
||||||
props: {
|
props: {
|
||||||
domainObject: {
|
domainObject: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
|
objectPath: {
|
||||||
|
type: Array,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
hasUnits: {
|
hasUnits: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
requred: true
|
requred: true
|
||||||
@ -65,7 +70,6 @@ export default {
|
|||||||
currentObjectPath.unshift(this.domainObject);
|
currentObjectPath.unshift(this.domainObject);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
name: this.domainObject.name,
|
|
||||||
timestamp: undefined,
|
timestamp: undefined,
|
||||||
value: '---',
|
value: '---',
|
||||||
valueClass: '',
|
valueClass: '',
|
||||||
@ -88,14 +92,6 @@ export default {
|
|||||||
.telemetry
|
.telemetry
|
||||||
.limitEvaluator(this.domainObject);
|
.limitEvaluator(this.domainObject);
|
||||||
|
|
||||||
this.stopWatchingMutation = this.openmct
|
|
||||||
.objects
|
|
||||||
.observe(
|
|
||||||
this.domainObject,
|
|
||||||
'*',
|
|
||||||
this.updateName
|
|
||||||
);
|
|
||||||
|
|
||||||
this.openmct.time.on('timeSystem', this.updateTimeSystem);
|
this.openmct.time.on('timeSystem', this.updateTimeSystem);
|
||||||
this.openmct.time.on('bounds', this.updateBounds);
|
this.openmct.time.on('bounds', this.updateBounds);
|
||||||
|
|
||||||
@ -118,7 +114,6 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
destroyed() {
|
destroyed() {
|
||||||
this.stopWatchingMutation();
|
|
||||||
this.unsubscribe();
|
this.unsubscribe();
|
||||||
this.openmct.time.off('timeSystem', this.updateTimeSystem);
|
this.openmct.time.off('timeSystem', this.updateTimeSystem);
|
||||||
this.openmct.time.off('bounds', this.updateBounds);
|
this.openmct.time.off('bounds', this.updateBounds);
|
||||||
@ -129,6 +124,7 @@ export default {
|
|||||||
let limit;
|
let limit;
|
||||||
|
|
||||||
if (this.shouldUpdate(newTimestamp)) {
|
if (this.shouldUpdate(newTimestamp)) {
|
||||||
|
this.datum = datum;
|
||||||
this.timestamp = newTimestamp;
|
this.timestamp = newTimestamp;
|
||||||
this.value = this.formats[this.valueKey].format(datum);
|
this.value = this.formats[this.valueKey].format(datum);
|
||||||
limit = this.limitEvaluator.evaluate(datum, this.valueMetadata);
|
limit = this.limitEvaluator.evaluate(datum, this.valueMetadata);
|
||||||
@ -158,9 +154,6 @@ export default {
|
|||||||
})
|
})
|
||||||
.then((array) => this.updateValues(array[array.length - 1]));
|
.then((array) => this.updateValues(array[array.length - 1]));
|
||||||
},
|
},
|
||||||
updateName(name) {
|
|
||||||
this.name = name;
|
|
||||||
},
|
|
||||||
updateBounds(bounds, isTick) {
|
updateBounds(bounds, isTick) {
|
||||||
this.bounds = bounds;
|
this.bounds = bounds;
|
||||||
if (!isTick) {
|
if (!isTick) {
|
||||||
@ -175,8 +168,25 @@ export default {
|
|||||||
this.resetValues();
|
this.resetValues();
|
||||||
this.timestampKey = timeSystem.key;
|
this.timestampKey = timeSystem.key;
|
||||||
},
|
},
|
||||||
|
getView() {
|
||||||
|
return {
|
||||||
|
getViewContext: () => {
|
||||||
|
return {
|
||||||
|
viewHistoricalData: true,
|
||||||
|
viewDatumAction: true,
|
||||||
|
getDatum: () => {
|
||||||
|
return this.datum;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
},
|
||||||
showContextMenu(event) {
|
showContextMenu(event) {
|
||||||
this.openmct.contextMenu._showContextMenuForObjectPath(this.currentObjectPath, event.x, event.y, CONTEXT_MENU_ACTIONS);
|
let actionCollection = this.openmct.actions.get(this.currentObjectPath, this.getView());
|
||||||
|
let allActions = actionCollection.getActionsObject();
|
||||||
|
let applicableActions = CONTEXT_MENU_ACTIONS.map(key => allActions[key]);
|
||||||
|
|
||||||
|
this.openmct.menus.showMenu(event.x, event.y, applicableActions);
|
||||||
},
|
},
|
||||||
resetValues() {
|
resetValues() {
|
||||||
this.value = '---';
|
this.value = '---';
|
||||||
|
@ -21,7 +21,7 @@
|
|||||||
*****************************************************************************/
|
*****************************************************************************/
|
||||||
|
|
||||||
<template>
|
<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">
|
<table class="c-table c-lad-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
@ -36,6 +36,7 @@
|
|||||||
v-for="item in items"
|
v-for="item in items"
|
||||||
:key="item.key"
|
:key="item.key"
|
||||||
:domain-object="item.domainObject"
|
:domain-object="item.domainObject"
|
||||||
|
:object-path="objectPath"
|
||||||
:has-units="hasUnits"
|
:has-units="hasUnits"
|
||||||
/>
|
/>
|
||||||
</tbody>
|
</tbody>
|
||||||
@ -47,10 +48,20 @@
|
|||||||
import LadRow from './LADRow.vue';
|
import LadRow from './LADRow.vue';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
inject: ['openmct', 'domainObject', 'objectPath'],
|
|
||||||
components: {
|
components: {
|
||||||
LadRow
|
LadRow
|
||||||
},
|
},
|
||||||
|
inject: ['openmct'],
|
||||||
|
props: {
|
||||||
|
domainObject: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
objectPath: {
|
||||||
|
type: Array,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
items: []
|
items: []
|
||||||
|
@ -57,10 +57,10 @@
|
|||||||
import LadRow from './LADRow.vue';
|
import LadRow from './LADRow.vue';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
inject: ['openmct', 'domainObject'],
|
|
||||||
components: {
|
components: {
|
||||||
LadRow
|
LadRow
|
||||||
},
|
},
|
||||||
|
inject: ['openmct', 'domainObject'],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
ladTableObjects: [],
|
ladTableObjects: [],
|
||||||
|
@ -19,342 +19,352 @@
|
|||||||
* this source code distribution or the Licensing information page available
|
* this source code distribution or the Licensing information page available
|
||||||
* at runtime from the About dialog for additional information.
|
* at runtime from the About dialog for additional information.
|
||||||
*****************************************************************************/
|
*****************************************************************************/
|
||||||
|
import AutoflowTabularPlugin from './AutoflowTabularPlugin';
|
||||||
|
import AutoflowTabularConstants from './AutoflowTabularConstants';
|
||||||
|
import $ from 'zepto';
|
||||||
|
import DOMObserver from './dom-observer';
|
||||||
|
import {
|
||||||
|
createOpenMct,
|
||||||
|
resetApplicationState,
|
||||||
|
spyOnBuiltins
|
||||||
|
} from 'utils/testing';
|
||||||
|
|
||||||
define([
|
describe("AutoflowTabularPlugin", () => {
|
||||||
'./AutoflowTabularPlugin',
|
let testType;
|
||||||
'./AutoflowTabularConstants',
|
let testObject;
|
||||||
'../../MCT',
|
let mockmct;
|
||||||
'zepto',
|
|
||||||
'./dom-observer'
|
|
||||||
], function (AutoflowTabularPlugin, AutoflowTabularConstants, MCT, $, DOMObserver) {
|
|
||||||
describe("AutoflowTabularPlugin", function () {
|
|
||||||
let testType;
|
|
||||||
let testObject;
|
|
||||||
let mockmct;
|
|
||||||
|
|
||||||
beforeEach(function () {
|
beforeEach(() => {
|
||||||
testType = "some-type";
|
testType = "some-type";
|
||||||
testObject = { type: testType };
|
testObject = { type: testType };
|
||||||
mockmct = new MCT();
|
mockmct = createOpenMct();
|
||||||
spyOn(mockmct.composition, 'get');
|
spyOn(mockmct.composition, 'get');
|
||||||
spyOn(mockmct.objectViews, 'addProvider');
|
spyOn(mockmct.objectViews, 'addProvider');
|
||||||
spyOn(mockmct.telemetry, 'getMetadata');
|
spyOn(mockmct.telemetry, 'getMetadata');
|
||||||
spyOn(mockmct.telemetry, 'getValueFormatter');
|
spyOn(mockmct.telemetry, 'getValueFormatter');
|
||||||
spyOn(mockmct.telemetry, 'limitEvaluator');
|
spyOn(mockmct.telemetry, 'limitEvaluator');
|
||||||
spyOn(mockmct.telemetry, 'request');
|
spyOn(mockmct.telemetry, 'request');
|
||||||
spyOn(mockmct.telemetry, 'subscribe');
|
spyOn(mockmct.telemetry, 'subscribe');
|
||||||
|
|
||||||
const plugin = new AutoflowTabularPlugin({ type: testType });
|
const plugin = new AutoflowTabularPlugin({ type: testType });
|
||||||
plugin(mockmct);
|
plugin(mockmct);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
resetApplicationState(mockmct);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("installs a view provider", () => {
|
||||||
|
expect(mockmct.objectViews.addProvider).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("installs a view provider which", () => {
|
||||||
|
let provider;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
provider =
|
||||||
|
mockmct.objectViews.addProvider.calls.mostRecent().args[0];
|
||||||
});
|
});
|
||||||
|
|
||||||
it("installs a view provider", function () {
|
it("applies its view to the type from options", () => {
|
||||||
expect(mockmct.objectViews.addProvider).toHaveBeenCalled();
|
expect(provider.canView(testObject)).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("installs a view provider which", function () {
|
it("does not apply to other types", () => {
|
||||||
let provider;
|
expect(provider.canView({ type: 'foo' })).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
beforeEach(function () {
|
describe("provides a view which", () => {
|
||||||
provider =
|
let testKeys;
|
||||||
mockmct.objectViews.addProvider.calls.mostRecent().args[0];
|
let testChildren;
|
||||||
});
|
let testContainer;
|
||||||
|
let testHistories;
|
||||||
|
let mockComposition;
|
||||||
|
let mockMetadata;
|
||||||
|
let mockEvaluator;
|
||||||
|
let mockUnsubscribes;
|
||||||
|
let callbacks;
|
||||||
|
let view;
|
||||||
|
let domObserver;
|
||||||
|
|
||||||
it("applies its view to the type from options", function () {
|
function waitsForChange() {
|
||||||
expect(provider.canView(testObject)).toBe(true);
|
return new Promise(function (resolve) {
|
||||||
});
|
window.requestAnimationFrame(resolve);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
it("does not apply to other types", function () {
|
function emitEvent(mockEmitter, type, event) {
|
||||||
expect(provider.canView({ type: 'foo' })).toBe(false);
|
mockEmitter.on.calls.all().forEach((call) => {
|
||||||
});
|
if (call.args[0] === type) {
|
||||||
|
call.args[1](event);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
describe("provides a view which", function () {
|
beforeEach((done) => {
|
||||||
let testKeys;
|
callbacks = {};
|
||||||
let testChildren;
|
|
||||||
let testContainer;
|
|
||||||
let testHistories;
|
|
||||||
let mockComposition;
|
|
||||||
let mockMetadata;
|
|
||||||
let mockEvaluator;
|
|
||||||
let mockUnsubscribes;
|
|
||||||
let callbacks;
|
|
||||||
let view;
|
|
||||||
let domObserver;
|
|
||||||
|
|
||||||
function waitsForChange() {
|
spyOnBuiltins(['requestAnimationFrame']);
|
||||||
return new Promise(function (resolve) {
|
window.requestAnimationFrame.and.callFake((callBack) => {
|
||||||
window.requestAnimationFrame(resolve);
|
callBack();
|
||||||
|
});
|
||||||
|
|
||||||
|
testObject = { type: 'some-type' };
|
||||||
|
testKeys = ['abc', 'def', 'xyz'];
|
||||||
|
testChildren = testKeys.map((key) => {
|
||||||
|
return {
|
||||||
|
identifier: {
|
||||||
|
namespace: "test",
|
||||||
|
key: key
|
||||||
|
},
|
||||||
|
name: "Object " + key
|
||||||
|
};
|
||||||
|
});
|
||||||
|
testContainer = $('<div>')[0];
|
||||||
|
domObserver = new DOMObserver(testContainer);
|
||||||
|
|
||||||
|
testHistories = testKeys.reduce((histories, key, index) => {
|
||||||
|
histories[key] = {
|
||||||
|
key: key,
|
||||||
|
range: index + 10,
|
||||||
|
domain: key + index
|
||||||
|
};
|
||||||
|
|
||||||
|
return histories;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
mockComposition =
|
||||||
|
jasmine.createSpyObj('composition', ['load', 'on', 'off']);
|
||||||
|
mockMetadata =
|
||||||
|
jasmine.createSpyObj('metadata', ['valuesForHints']);
|
||||||
|
|
||||||
|
mockEvaluator = jasmine.createSpyObj('evaluator', ['evaluate']);
|
||||||
|
mockUnsubscribes = testKeys.reduce((map, key) => {
|
||||||
|
map[key] = jasmine.createSpy('unsubscribe-' + key);
|
||||||
|
|
||||||
|
return map;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
mockmct.composition.get.and.returnValue(mockComposition);
|
||||||
|
mockComposition.load.and.callFake(() => {
|
||||||
|
testChildren.forEach(emitEvent.bind(null, mockComposition, 'add'));
|
||||||
|
|
||||||
|
return Promise.resolve(testChildren);
|
||||||
|
});
|
||||||
|
|
||||||
|
mockmct.telemetry.getMetadata.and.returnValue(mockMetadata);
|
||||||
|
mockmct.telemetry.getValueFormatter.and.callFake((metadatum) => {
|
||||||
|
const mockFormatter = jasmine.createSpyObj('formatter', ['format']);
|
||||||
|
mockFormatter.format.and.callFake((datum) => {
|
||||||
|
return datum[metadatum.hint];
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return mockFormatter;
|
||||||
|
});
|
||||||
|
mockmct.telemetry.limitEvaluator.and.returnValue(mockEvaluator);
|
||||||
|
mockmct.telemetry.subscribe.and.callFake((obj, callback) => {
|
||||||
|
const key = obj.identifier.key;
|
||||||
|
callbacks[key] = callback;
|
||||||
|
|
||||||
|
return mockUnsubscribes[key];
|
||||||
|
});
|
||||||
|
mockmct.telemetry.request.and.callFake((obj, request) => {
|
||||||
|
const key = obj.identifier.key;
|
||||||
|
|
||||||
|
return Promise.resolve([testHistories[key]]);
|
||||||
|
});
|
||||||
|
mockMetadata.valuesForHints.and.callFake((hints) => {
|
||||||
|
return [{ hint: hints[0] }];
|
||||||
|
});
|
||||||
|
|
||||||
|
view = provider.view(testObject);
|
||||||
|
view.show(testContainer);
|
||||||
|
|
||||||
|
return done();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
domObserver.destroy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("populates its container", () => {
|
||||||
|
expect(testContainer.children.length > 0).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when rows have been populated", () => {
|
||||||
|
function rowsMatch() {
|
||||||
|
const rows = $(testContainer).find(".l-autoflow-row").length;
|
||||||
|
|
||||||
|
return rows === testChildren.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
function emitEvent(mockEmitter, type, event) {
|
it("shows one row per child object", () => {
|
||||||
mockEmitter.on.calls.all().forEach(function (call) {
|
return domObserver.when(rowsMatch);
|
||||||
if (call.args[0] === type) {
|
});
|
||||||
call.args[1](event);
|
|
||||||
}
|
// it("adds rows on composition change", () => {
|
||||||
});
|
// const child = {
|
||||||
|
// identifier: {
|
||||||
|
// namespace: "test",
|
||||||
|
// key: "123"
|
||||||
|
// },
|
||||||
|
// name: "Object 123"
|
||||||
|
// };
|
||||||
|
// testChildren.push(child);
|
||||||
|
// emitEvent(mockComposition, 'add', child);
|
||||||
|
|
||||||
|
// return domObserver.when(rowsMatch);
|
||||||
|
// });
|
||||||
|
|
||||||
|
it("removes rows on composition change", () => {
|
||||||
|
const child = testChildren.pop();
|
||||||
|
emitEvent(mockComposition, 'remove', child.identifier);
|
||||||
|
|
||||||
|
return domObserver.when(rowsMatch);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("removes subscriptions when destroyed", () => {
|
||||||
|
testKeys.forEach((key) => {
|
||||||
|
expect(mockUnsubscribes[key]).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
view.destroy();
|
||||||
|
testKeys.forEach((key) => {
|
||||||
|
expect(mockUnsubscribes[key]).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("provides a button to change column width", () => {
|
||||||
|
const initialWidth = AutoflowTabularConstants.INITIAL_COLUMN_WIDTH;
|
||||||
|
const nextWidth =
|
||||||
|
initialWidth + AutoflowTabularConstants.COLUMN_WIDTH_STEP;
|
||||||
|
|
||||||
|
expect($(testContainer).find('.l-autoflow-col').css('width'))
|
||||||
|
.toEqual(initialWidth + 'px');
|
||||||
|
|
||||||
|
$(testContainer).find('.change-column-width').click();
|
||||||
|
|
||||||
|
function widthHasChanged() {
|
||||||
|
const width = $(testContainer).find('.l-autoflow-col').css('width');
|
||||||
|
|
||||||
|
return width !== initialWidth + 'px';
|
||||||
}
|
}
|
||||||
|
|
||||||
beforeEach(function () {
|
return domObserver.when(widthHasChanged)
|
||||||
callbacks = {};
|
.then(() => {
|
||||||
|
expect($(testContainer).find('.l-autoflow-col').css('width'))
|
||||||
testObject = { type: 'some-type' };
|
.toEqual(nextWidth + 'px');
|
||||||
testKeys = ['abc', 'def', 'xyz'];
|
|
||||||
testChildren = testKeys.map(function (key) {
|
|
||||||
return {
|
|
||||||
identifier: {
|
|
||||||
namespace: "test",
|
|
||||||
key: key
|
|
||||||
},
|
|
||||||
name: "Object " + key
|
|
||||||
};
|
|
||||||
});
|
});
|
||||||
testContainer = $('<div>')[0];
|
});
|
||||||
domObserver = new DOMObserver(testContainer);
|
|
||||||
|
|
||||||
testHistories = testKeys.reduce(function (histories, key, index) {
|
it("subscribes to all child objects", () => {
|
||||||
histories[key] = {
|
testKeys.forEach((key) => {
|
||||||
key: key,
|
expect(callbacks[key]).toEqual(jasmine.any(Function));
|
||||||
range: index + 10,
|
});
|
||||||
domain: key + index
|
});
|
||||||
};
|
|
||||||
|
|
||||||
return histories;
|
it("displays historical telemetry", () => {
|
||||||
}, {});
|
function rowTextDefined() {
|
||||||
|
return $(testContainer).find(".l-autoflow-item").filter(".r").text() !== "";
|
||||||
|
}
|
||||||
|
|
||||||
mockComposition =
|
return domObserver.when(rowTextDefined).then(() => {
|
||||||
jasmine.createSpyObj('composition', ['load', 'on', 'off']);
|
testKeys.forEach((key, index) => {
|
||||||
mockMetadata =
|
const datum = testHistories[key];
|
||||||
jasmine.createSpyObj('metadata', ['valuesForHints']);
|
const $cell = $(testContainer).find(".l-autoflow-row").eq(index).find(".r");
|
||||||
|
expect($cell.text()).toEqual(String(datum.range));
|
||||||
mockEvaluator = jasmine.createSpyObj('evaluator', ['evaluate']);
|
|
||||||
mockUnsubscribes = testKeys.reduce(function (map, key) {
|
|
||||||
map[key] = jasmine.createSpy('unsubscribe-' + key);
|
|
||||||
|
|
||||||
return map;
|
|
||||||
}, {});
|
|
||||||
|
|
||||||
mockmct.composition.get.and.returnValue(mockComposition);
|
|
||||||
mockComposition.load.and.callFake(function () {
|
|
||||||
testChildren.forEach(emitEvent.bind(null, mockComposition, 'add'));
|
|
||||||
|
|
||||||
return Promise.resolve(testChildren);
|
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
mockmct.telemetry.getMetadata.and.returnValue(mockMetadata);
|
it("displays incoming telemetry", () => {
|
||||||
mockmct.telemetry.getValueFormatter.and.callFake(function (metadatum) {
|
const testData = testKeys.map((key, index) => {
|
||||||
const mockFormatter = jasmine.createSpyObj('formatter', ['format']);
|
return {
|
||||||
mockFormatter.format.and.callFake(function (datum) {
|
key: key,
|
||||||
return datum[metadatum.hint];
|
range: index * 100,
|
||||||
});
|
domain: key + index
|
||||||
|
};
|
||||||
return mockFormatter;
|
|
||||||
});
|
|
||||||
mockmct.telemetry.limitEvaluator.and.returnValue(mockEvaluator);
|
|
||||||
mockmct.telemetry.subscribe.and.callFake(function (obj, callback) {
|
|
||||||
const key = obj.identifier.key;
|
|
||||||
callbacks[key] = callback;
|
|
||||||
|
|
||||||
return mockUnsubscribes[key];
|
|
||||||
});
|
|
||||||
mockmct.telemetry.request.and.callFake(function (obj, request) {
|
|
||||||
const key = obj.identifier.key;
|
|
||||||
|
|
||||||
return Promise.resolve([testHistories[key]]);
|
|
||||||
});
|
|
||||||
mockMetadata.valuesForHints.and.callFake(function (hints) {
|
|
||||||
return [{ hint: hints[0] }];
|
|
||||||
});
|
|
||||||
|
|
||||||
view = provider.view(testObject);
|
|
||||||
view.show(testContainer);
|
|
||||||
|
|
||||||
return waitsForChange();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(function () {
|
testData.forEach((datum) => {
|
||||||
domObserver.destroy();
|
callbacks[datum.key](datum);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("populates its container", function () {
|
return waitsForChange().then(() => {
|
||||||
expect(testContainer.children.length > 0).toBe(true);
|
testData.forEach((datum, index) => {
|
||||||
|
const $cell = $(testContainer).find(".l-autoflow-row").eq(index).find(".r");
|
||||||
|
expect($cell.text()).toEqual(String(datum.range));
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("when rows have been populated", function () {
|
it("updates classes for limit violations", () => {
|
||||||
function rowsMatch() {
|
const testClass = "some-limit-violation";
|
||||||
const rows = $(testContainer).find(".l-autoflow-row").length;
|
mockEvaluator.evaluate.and.returnValue({ cssClass: testClass });
|
||||||
|
testKeys.forEach((key) => {
|
||||||
return rows === testChildren.length;
|
callbacks[key]({
|
||||||
}
|
range: 'foo',
|
||||||
|
domain: 'bar'
|
||||||
it("shows one row per child object", function () {
|
|
||||||
return domObserver.when(rowsMatch);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("adds rows on composition change", function () {
|
|
||||||
const child = {
|
|
||||||
identifier: {
|
|
||||||
namespace: "test",
|
|
||||||
key: "123"
|
|
||||||
},
|
|
||||||
name: "Object 123"
|
|
||||||
};
|
|
||||||
testChildren.push(child);
|
|
||||||
emitEvent(mockComposition, 'add', child);
|
|
||||||
|
|
||||||
return domObserver.when(rowsMatch);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("removes rows on composition change", function () {
|
|
||||||
const child = testChildren.pop();
|
|
||||||
emitEvent(mockComposition, 'remove', child.identifier);
|
|
||||||
|
|
||||||
return domObserver.when(rowsMatch);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("removes subscriptions when destroyed", function () {
|
return waitsForChange().then(() => {
|
||||||
testKeys.forEach(function (key) {
|
testKeys.forEach((datum, index) => {
|
||||||
expect(mockUnsubscribes[key]).not.toHaveBeenCalled();
|
const $cell = $(testContainer).find(".l-autoflow-row").eq(index).find(".r");
|
||||||
});
|
expect($cell.hasClass(testClass)).toBe(true);
|
||||||
view.destroy();
|
|
||||||
testKeys.forEach(function (key) {
|
|
||||||
expect(mockUnsubscribes[key]).toHaveBeenCalled();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it("provides a button to change column width", function () {
|
it("automatically flows to new columns", () => {
|
||||||
const initialWidth = AutoflowTabularConstants.INITIAL_COLUMN_WIDTH;
|
const rowHeight = AutoflowTabularConstants.ROW_HEIGHT;
|
||||||
const nextWidth =
|
const sliderHeight = AutoflowTabularConstants.SLIDER_HEIGHT;
|
||||||
initialWidth + AutoflowTabularConstants.COLUMN_WIDTH_STEP;
|
const count = testKeys.length;
|
||||||
|
const $container = $(testContainer);
|
||||||
|
let promiseChain = Promise.resolve();
|
||||||
|
|
||||||
expect($(testContainer).find('.l-autoflow-col').css('width'))
|
function columnsHaveAutoflowed() {
|
||||||
.toEqual(initialWidth + 'px');
|
const itemsHeight = $container.find('.l-autoflow-items').height();
|
||||||
|
const availableHeight = itemsHeight - sliderHeight;
|
||||||
|
const availableRows = Math.max(Math.floor(availableHeight / rowHeight), 1);
|
||||||
|
const columns = Math.ceil(count / availableRows);
|
||||||
|
|
||||||
$(testContainer).find('.change-column-width').click();
|
return $container.find('.l-autoflow-col').length === columns;
|
||||||
|
}
|
||||||
|
|
||||||
function widthHasChanged() {
|
$container.find('.abs').css({
|
||||||
const width = $(testContainer).find('.l-autoflow-col').css('width');
|
position: 'absolute',
|
||||||
|
left: '0px',
|
||||||
return width !== initialWidth + 'px';
|
right: '0px',
|
||||||
}
|
top: '0px',
|
||||||
|
bottom: '0px'
|
||||||
return domObserver.when(widthHasChanged)
|
|
||||||
.then(function () {
|
|
||||||
expect($(testContainer).find('.l-autoflow-col').css('width'))
|
|
||||||
.toEqual(nextWidth + 'px');
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
$container.css({ position: 'absolute' });
|
||||||
|
|
||||||
it("subscribes to all child objects", function () {
|
$container.appendTo(document.body);
|
||||||
testKeys.forEach(function (key) {
|
|
||||||
expect(callbacks[key]).toEqual(jasmine.any(Function));
|
function setHeight(height) {
|
||||||
});
|
$container.css('height', height + 'px');
|
||||||
|
|
||||||
|
return domObserver.when(columnsHaveAutoflowed);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let height = 0; height < rowHeight * count * 2; height += rowHeight / 2) {
|
||||||
|
// eslint-disable-next-line no-invalid-this
|
||||||
|
promiseChain = promiseChain.then(setHeight.bind(this, height));
|
||||||
|
}
|
||||||
|
|
||||||
|
return promiseChain.then(() => {
|
||||||
|
$container.remove();
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it("displays historical telemetry", function () {
|
it("loads composition exactly once", () => {
|
||||||
function rowTextDefined() {
|
const testObj = testChildren.pop();
|
||||||
return $(testContainer).find(".l-autoflow-item").filter(".r").text() !== "";
|
emitEvent(mockComposition, 'remove', testObj.identifier);
|
||||||
}
|
testChildren.push(testObj);
|
||||||
|
emitEvent(mockComposition, 'add', testObj);
|
||||||
return domObserver.when(rowTextDefined).then(function () {
|
expect(mockComposition.load.calls.count()).toEqual(1);
|
||||||
testKeys.forEach(function (key, index) {
|
|
||||||
const datum = testHistories[key];
|
|
||||||
const $cell = $(testContainer).find(".l-autoflow-row").eq(index).find(".r");
|
|
||||||
expect($cell.text()).toEqual(String(datum.range));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("displays incoming telemetry", function () {
|
|
||||||
const testData = testKeys.map(function (key, index) {
|
|
||||||
return {
|
|
||||||
key: key,
|
|
||||||
range: index * 100,
|
|
||||||
domain: key + index
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
testData.forEach(function (datum) {
|
|
||||||
callbacks[datum.key](datum);
|
|
||||||
});
|
|
||||||
|
|
||||||
return waitsForChange().then(function () {
|
|
||||||
testData.forEach(function (datum, index) {
|
|
||||||
const $cell = $(testContainer).find(".l-autoflow-row").eq(index).find(".r");
|
|
||||||
expect($cell.text()).toEqual(String(datum.range));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("updates classes for limit violations", function () {
|
|
||||||
const testClass = "some-limit-violation";
|
|
||||||
mockEvaluator.evaluate.and.returnValue({ cssClass: testClass });
|
|
||||||
testKeys.forEach(function (key) {
|
|
||||||
callbacks[key]({
|
|
||||||
range: 'foo',
|
|
||||||
domain: 'bar'
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return waitsForChange().then(function () {
|
|
||||||
testKeys.forEach(function (datum, index) {
|
|
||||||
const $cell = $(testContainer).find(".l-autoflow-row").eq(index).find(".r");
|
|
||||||
expect($cell.hasClass(testClass)).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("automatically flows to new columns", function () {
|
|
||||||
const rowHeight = AutoflowTabularConstants.ROW_HEIGHT;
|
|
||||||
const sliderHeight = AutoflowTabularConstants.SLIDER_HEIGHT;
|
|
||||||
const count = testKeys.length;
|
|
||||||
const $container = $(testContainer);
|
|
||||||
let promiseChain = Promise.resolve();
|
|
||||||
|
|
||||||
function columnsHaveAutoflowed() {
|
|
||||||
const itemsHeight = $container.find('.l-autoflow-items').height();
|
|
||||||
const availableHeight = itemsHeight - sliderHeight;
|
|
||||||
const availableRows = Math.max(Math.floor(availableHeight / rowHeight), 1);
|
|
||||||
const columns = Math.ceil(count / availableRows);
|
|
||||||
|
|
||||||
return $container.find('.l-autoflow-col').length === columns;
|
|
||||||
}
|
|
||||||
|
|
||||||
$container.find('.abs').css({
|
|
||||||
position: 'absolute',
|
|
||||||
left: '0px',
|
|
||||||
right: '0px',
|
|
||||||
top: '0px',
|
|
||||||
bottom: '0px'
|
|
||||||
});
|
|
||||||
$container.css({ position: 'absolute' });
|
|
||||||
|
|
||||||
$container.appendTo(document.body);
|
|
||||||
|
|
||||||
function setHeight(height) {
|
|
||||||
$container.css('height', height + 'px');
|
|
||||||
|
|
||||||
return domObserver.when(columnsHaveAutoflowed);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let height = 0; height < rowHeight * count * 2; height += rowHeight / 2) {
|
|
||||||
// eslint-disable-next-line no-invalid-this
|
|
||||||
promiseChain = promiseChain.then(setHeight.bind(this, height));
|
|
||||||
}
|
|
||||||
|
|
||||||
return promiseChain.then(function () {
|
|
||||||
$container.remove();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("loads composition exactly once", function () {
|
|
||||||
const testObj = testChildren.pop();
|
|
||||||
emitEvent(mockComposition, 'remove', testObj.identifier);
|
|
||||||
testChildren.push(testObj);
|
|
||||||
emitEvent(mockComposition, 'add', testObj);
|
|
||||||
expect(mockComposition.load.calls.count()).toEqual(1);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -23,6 +23,7 @@
|
|||||||
export default class ClearDataAction {
|
export default class ClearDataAction {
|
||||||
constructor(openmct, appliesToObjects) {
|
constructor(openmct, appliesToObjects) {
|
||||||
this.name = 'Clear Data for Object';
|
this.name = 'Clear Data for Object';
|
||||||
|
this.key = 'clear-data-action';
|
||||||
this.description = 'Clears current data for object, unsubscribes and resubscribes to data';
|
this.description = 'Clears current data for object, unsubscribes and resubscribes to data';
|
||||||
this.cssClass = 'icon-clear-data';
|
this.cssClass = 'icon-clear-data';
|
||||||
|
|
||||||
|
@ -37,12 +37,12 @@ define([
|
|||||||
return function install(openmct) {
|
return function install(openmct) {
|
||||||
if (installIndicator) {
|
if (installIndicator) {
|
||||||
let component = new Vue ({
|
let component = new Vue ({
|
||||||
provide: {
|
|
||||||
openmct
|
|
||||||
},
|
|
||||||
components: {
|
components: {
|
||||||
GlobalClearIndicator: GlobaClearIndicator.default
|
GlobalClearIndicator: GlobaClearIndicator.default
|
||||||
},
|
},
|
||||||
|
provide: {
|
||||||
|
openmct
|
||||||
|
},
|
||||||
template: '<GlobalClearIndicator></GlobalClearIndicator>'
|
template: '<GlobalClearIndicator></GlobalClearIndicator>'
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -53,7 +53,7 @@ define([
|
|||||||
openmct.indicators.add(indicator);
|
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 () {
|
describe('When the Clear Data Plugin is installed,', function () {
|
||||||
const mockObjectViews = jasmine.createSpyObj('objectViews', ['emit']);
|
const mockObjectViews = jasmine.createSpyObj('objectViews', ['emit']);
|
||||||
const mockIndicatorProvider = jasmine.createSpyObj('indicators', ['add']);
|
const mockIndicatorProvider = jasmine.createSpyObj('indicators', ['add']);
|
||||||
const mockContextMenuProvider = jasmine.createSpyObj('contextMenu', ['registerAction']);
|
const mockActionsProvider = jasmine.createSpyObj('actions', ['register']);
|
||||||
|
|
||||||
const openmct = {
|
const openmct = {
|
||||||
objectViews: mockObjectViews,
|
objectViews: mockObjectViews,
|
||||||
indicators: mockIndicatorProvider,
|
indicators: mockIndicatorProvider,
|
||||||
contextMenu: mockContextMenuProvider,
|
actions: mockActionsProvider,
|
||||||
install: function (plugin) {
|
install: function (plugin) {
|
||||||
plugin(this);
|
plugin(this);
|
||||||
}
|
}
|
||||||
@ -51,7 +51,7 @@ describe('When the Clear Data Plugin is installed,', function () {
|
|||||||
it('Clear Data context menu action is installed', function () {
|
it('Clear Data context menu action is installed', function () {
|
||||||
openmct.install(ClearDataActionPlugin([]));
|
openmct.install(ClearDataActionPlugin([]));
|
||||||
|
|
||||||
expect(mockContextMenuProvider.registerAction).toHaveBeenCalled();
|
expect(mockActionsProvider.register).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('clear data action emits a clearData event when invoked', function () {
|
it('clear data action emits a clearData event when invoked', function () {
|
||||||
|
@ -75,7 +75,8 @@ export default class Condition extends EventEmitter {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.isTelemetryUsed(datum.id)) {
|
// if all the criteria in this condition have no telemetry, we want to force the condition result to evaluate
|
||||||
|
if (this.hasNoTelemetry() || this.isTelemetryUsed(datum.id)) {
|
||||||
|
|
||||||
this.criteria.forEach(criterion => {
|
this.criteria.forEach(criterion => {
|
||||||
if (this.isAnyOrAllTelemetry(criterion)) {
|
if (this.isAnyOrAllTelemetry(criterion)) {
|
||||||
@ -93,6 +94,12 @@ export default class Condition extends EventEmitter {
|
|||||||
return (criterion.telemetry && (criterion.telemetry === 'all' || criterion.telemetry === 'any'));
|
return (criterion.telemetry && (criterion.telemetry === 'all' || criterion.telemetry === 'any'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
hasNoTelemetry() {
|
||||||
|
return this.criteria.every((criterion) => {
|
||||||
|
return !this.isAnyOrAllTelemetry(criterion) && criterion.telemetry === '';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
isTelemetryUsed(id) {
|
isTelemetryUsed(id) {
|
||||||
return this.criteria.some(criterion => {
|
return this.criteria.some(criterion => {
|
||||||
return this.isAnyOrAllTelemetry(criterion) || criterion.telemetryObjectIdAsString === id;
|
return this.isAnyOrAllTelemetry(criterion) || criterion.telemetryObjectIdAsString === id;
|
||||||
@ -250,10 +257,17 @@ export default class Condition extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getTriggerDescription() {
|
getTriggerDescription() {
|
||||||
return {
|
if (this.trigger) {
|
||||||
conjunction: TRIGGER_CONJUNCTION[this.trigger],
|
return {
|
||||||
prefix: `${TRIGGER_LABEL[this.trigger]}: `
|
conjunction: TRIGGER_CONJUNCTION[this.trigger],
|
||||||
};
|
prefix: `${TRIGGER_LABEL[this.trigger]}: `
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
conjunction: '',
|
||||||
|
prefix: ''
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
requestLADConditionResult() {
|
requestLADConditionResult() {
|
||||||
|
@ -34,6 +34,9 @@ export default class ConditionManager extends EventEmitter {
|
|||||||
this.composition = this.openmct.composition.get(conditionSetDomainObject);
|
this.composition = this.openmct.composition.get(conditionSetDomainObject);
|
||||||
this.composition.on('add', this.subscribeToTelemetry, this);
|
this.composition.on('add', this.subscribeToTelemetry, this);
|
||||||
this.composition.on('remove', this.unsubscribeFromTelemetry, this);
|
this.composition.on('remove', this.unsubscribeFromTelemetry, this);
|
||||||
|
|
||||||
|
this.shouldEvaluateNewTelemetry = this.shouldEvaluateNewTelemetry.bind(this);
|
||||||
|
|
||||||
this.compositionLoad = this.composition.load();
|
this.compositionLoad = this.composition.load();
|
||||||
this.subscriptions = {};
|
this.subscriptions = {};
|
||||||
this.telemetryObjects = {};
|
this.telemetryObjects = {};
|
||||||
@ -79,6 +82,17 @@ export default class ConditionManager extends EventEmitter {
|
|||||||
delete this.subscriptions[id];
|
delete this.subscriptions[id];
|
||||||
delete this.telemetryObjects[id];
|
delete this.telemetryObjects[id];
|
||||||
this.removeConditionTelemetryObjects();
|
this.removeConditionTelemetryObjects();
|
||||||
|
|
||||||
|
//force re-computation of condition set result as we might be in a state where
|
||||||
|
// there is no telemetry datum coming in for a while or at all.
|
||||||
|
let latestTimestamp = getLatestTimestamp(
|
||||||
|
{},
|
||||||
|
{},
|
||||||
|
this.timeSystems,
|
||||||
|
this.openmct.time.timeSystem()
|
||||||
|
);
|
||||||
|
this.updateConditionResults({id: id});
|
||||||
|
this.updateCurrentCondition(latestTimestamp);
|
||||||
}
|
}
|
||||||
|
|
||||||
initialize() {
|
initialize() {
|
||||||
@ -326,6 +340,10 @@ export default class ConditionManager extends EventEmitter {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
shouldEvaluateNewTelemetry(currentTimestamp) {
|
||||||
|
return this.openmct.time.bounds().end >= currentTimestamp;
|
||||||
|
}
|
||||||
|
|
||||||
telemetryReceived(endpoint, datum) {
|
telemetryReceived(endpoint, datum) {
|
||||||
if (!this.isTelemetryUsed(endpoint)) {
|
if (!this.isTelemetryUsed(endpoint)) {
|
||||||
return;
|
return;
|
||||||
@ -334,16 +352,21 @@ export default class ConditionManager extends EventEmitter {
|
|||||||
const normalizedDatum = this.createNormalizedDatum(datum, endpoint);
|
const normalizedDatum = this.createNormalizedDatum(datum, endpoint);
|
||||||
const timeSystemKey = this.openmct.time.timeSystem().key;
|
const timeSystemKey = this.openmct.time.timeSystem().key;
|
||||||
let timestamp = {};
|
let timestamp = {};
|
||||||
timestamp[timeSystemKey] = normalizedDatum[timeSystemKey];
|
const currentTimestamp = normalizedDatum[timeSystemKey];
|
||||||
|
timestamp[timeSystemKey] = currentTimestamp;
|
||||||
|
if (this.shouldEvaluateNewTelemetry(currentTimestamp)) {
|
||||||
|
this.updateConditionResults(normalizedDatum);
|
||||||
|
this.updateCurrentCondition(timestamp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateConditionResults(normalizedDatum) {
|
||||||
//We want to stop when the first condition evaluates to true.
|
//We want to stop when the first condition evaluates to true.
|
||||||
this.conditions.some((condition) => {
|
this.conditions.some((condition) => {
|
||||||
condition.updateResult(normalizedDatum);
|
condition.updateResult(normalizedDatum);
|
||||||
|
|
||||||
return condition.result === true;
|
return condition.result === true;
|
||||||
});
|
});
|
||||||
|
|
||||||
this.updateCurrentCondition(timestamp);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
updateCurrentCondition(timestamp) {
|
updateCurrentCondition(timestamp) {
|
||||||
|
@ -86,6 +86,7 @@ export default class StyleRuleManager extends EventEmitter {
|
|||||||
updateObjectStyleConfig(styleConfiguration) {
|
updateObjectStyleConfig(styleConfiguration) {
|
||||||
if (!styleConfiguration || !styleConfiguration.conditionSetIdentifier) {
|
if (!styleConfiguration || !styleConfiguration.conditionSetIdentifier) {
|
||||||
this.initialize(styleConfiguration || {});
|
this.initialize(styleConfiguration || {});
|
||||||
|
this.applyStaticStyle();
|
||||||
this.destroy();
|
this.destroy();
|
||||||
} else {
|
} else {
|
||||||
let isNewConditionSet = !this.conditionSetIdentifier
|
let isNewConditionSet = !this.conditionSetIdentifier
|
||||||
@ -158,7 +159,6 @@ export default class StyleRuleManager extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
destroy() {
|
destroy() {
|
||||||
this.applyStaticStyle();
|
|
||||||
if (this.stopProvidingTelemetry) {
|
if (this.stopProvidingTelemetry) {
|
||||||
this.stopProvidingTelemetry();
|
this.stopProvidingTelemetry();
|
||||||
delete this.stopProvidingTelemetry;
|
delete this.stopProvidingTelemetry;
|
||||||
|
@ -195,11 +195,11 @@ import { TRIGGER, TRIGGER_LABEL } from "@/plugins/condition/utils/constants";
|
|||||||
import uuid from 'uuid';
|
import uuid from 'uuid';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
inject: ['openmct'],
|
|
||||||
components: {
|
components: {
|
||||||
Criterion,
|
Criterion,
|
||||||
ConditionDescription
|
ConditionDescription
|
||||||
},
|
},
|
||||||
|
inject: ['openmct'],
|
||||||
props: {
|
props: {
|
||||||
currentConditionId: {
|
currentConditionId: {
|
||||||
type: String,
|
type: String,
|
||||||
|
@ -81,10 +81,10 @@ import Condition from './Condition.vue';
|
|||||||
import ConditionManager from '../ConditionManager';
|
import ConditionManager from '../ConditionManager';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
inject: ['openmct', 'domainObject'],
|
|
||||||
components: {
|
components: {
|
||||||
Condition
|
Condition
|
||||||
},
|
},
|
||||||
|
inject: ['openmct', 'domainObject'],
|
||||||
props: {
|
props: {
|
||||||
isEditing: Boolean,
|
isEditing: Boolean,
|
||||||
testData: {
|
testData: {
|
||||||
|
@ -58,11 +58,11 @@ import TestData from './TestData.vue';
|
|||||||
import ConditionCollection from './ConditionCollection.vue';
|
import ConditionCollection from './ConditionCollection.vue';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
inject: ["openmct", "domainObject"],
|
|
||||||
components: {
|
components: {
|
||||||
TestData,
|
TestData,
|
||||||
ConditionCollection
|
ConditionCollection
|
||||||
},
|
},
|
||||||
|
inject: ["openmct", "domainObject"],
|
||||||
props: {
|
props: {
|
||||||
isEditing: Boolean
|
isEditing: Boolean
|
||||||
},
|
},
|
||||||
|
@ -50,6 +50,7 @@
|
|||||||
.c-cs {
|
.c-cs {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
flex: 1 1 auto;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
|
@ -31,7 +31,6 @@
|
|||||||
v-model="expanded"
|
v-model="expanded"
|
||||||
class="c-tree__item__view-control"
|
class="c-tree__item__view-control"
|
||||||
:enabled="hasChildren"
|
:enabled="hasChildren"
|
||||||
:propagate="false"
|
|
||||||
/>
|
/>
|
||||||
<div class="c-tree__item__label c-object-label">
|
<div class="c-tree__item__label c-object-label">
|
||||||
<div
|
<div
|
||||||
@ -42,7 +41,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ul
|
<ul
|
||||||
v-if="expanded"
|
v-if="expanded && !isLoading"
|
||||||
class="c-tree"
|
class="c-tree"
|
||||||
>
|
>
|
||||||
<li
|
<li
|
||||||
@ -69,10 +68,10 @@ import viewControl from '@/ui/components/viewControl.vue';
|
|||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'ConditionSetDialogTreeItem',
|
name: 'ConditionSetDialogTreeItem',
|
||||||
inject: ['openmct'],
|
|
||||||
components: {
|
components: {
|
||||||
viewControl
|
viewControl
|
||||||
},
|
},
|
||||||
|
inject: ['openmct'],
|
||||||
props: {
|
props: {
|
||||||
node: {
|
node: {
|
||||||
type: Object,
|
type: Object,
|
||||||
|
@ -41,7 +41,7 @@
|
|||||||
></div>
|
></div>
|
||||||
<!-- end loading -->
|
<!-- end loading -->
|
||||||
|
|
||||||
<div v-if="(allTreeItems.length === 0) || (searchValue && filteredTreeItems.length === 0)"
|
<div v-if="shouldDisplayNoResultsText"
|
||||||
class="c-tree-and-search__no-results"
|
class="c-tree-and-search__no-results"
|
||||||
>
|
>
|
||||||
No results found
|
No results found
|
||||||
@ -63,7 +63,7 @@
|
|||||||
<!-- end main tree -->
|
<!-- end main tree -->
|
||||||
|
|
||||||
<!-- search tree -->
|
<!-- search tree -->
|
||||||
<ul v-if="searchValue"
|
<ul v-if="searchValue && !isLoading"
|
||||||
class="c-tree-and-search__tree c-tree"
|
class="c-tree-and-search__tree c-tree"
|
||||||
>
|
>
|
||||||
<condition-set-dialog-tree-item
|
<condition-set-dialog-tree-item
|
||||||
@ -80,16 +80,17 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import debounce from 'lodash/debounce';
|
||||||
import search from '@/ui/components/search.vue';
|
import search from '@/ui/components/search.vue';
|
||||||
import ConditionSetDialogTreeItem from './ConditionSetDialogTreeItem.vue';
|
import ConditionSetDialogTreeItem from './ConditionSetDialogTreeItem.vue';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
inject: ['openmct'],
|
|
||||||
name: 'ConditionSetSelectorDialog',
|
name: 'ConditionSetSelectorDialog',
|
||||||
components: {
|
components: {
|
||||||
search,
|
search,
|
||||||
ConditionSetDialogTreeItem
|
ConditionSetDialogTreeItem
|
||||||
},
|
},
|
||||||
|
inject: ['openmct'],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
expanded: false,
|
expanded: false,
|
||||||
@ -100,8 +101,20 @@ export default {
|
|||||||
selectedItem: undefined
|
selectedItem: undefined
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
computed: {
|
||||||
|
shouldDisplayNoResultsText() {
|
||||||
|
if (this.isLoading) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.allTreeItems.length === 0
|
||||||
|
|| (this.searchValue && this.filteredTreeItems.length === 0);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.getDebouncedFilteredChildren = debounce(this.getFilteredChildren, 400);
|
||||||
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.searchService = this.openmct.$injector.get('searchService');
|
|
||||||
this.getAllChildren();
|
this.getAllChildren();
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@ -124,37 +137,44 @@ export default {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
getFilteredChildren() {
|
getFilteredChildren() {
|
||||||
this.searchService.query(this.searchValue).then(children => {
|
// clear any previous search results
|
||||||
this.filteredTreeItems = children.hits.map(child => {
|
this.filteredTreeItems = [];
|
||||||
|
|
||||||
let context = child.object.getCapability('context');
|
const promises = this.openmct.objects.search(this.searchValue)
|
||||||
let object = child.object.useCapability('adapter');
|
.map(promise => promise
|
||||||
let objectPath = [];
|
.then(results => this.aggregateFilteredChildren(results)));
|
||||||
let navigateToParent;
|
|
||||||
|
|
||||||
if (context) {
|
Promise.all(promises).then(() => {
|
||||||
objectPath = context.getPath().slice(1)
|
this.isLoading = false;
|
||||||
.map(oldObject => oldObject.useCapability('adapter'))
|
|
||||||
.reverse();
|
|
||||||
navigateToParent = '/browse/' + objectPath.slice(1)
|
|
||||||
.map((parent) => this.openmct.objects.makeKeyString(parent.identifier))
|
|
||||||
.join('/');
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: this.openmct.objects.makeKeyString(object.identifier),
|
|
||||||
object,
|
|
||||||
objectPath,
|
|
||||||
navigateToParent
|
|
||||||
};
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
async aggregateFilteredChildren(results) {
|
||||||
|
for (const object of results) {
|
||||||
|
const objectPath = await this.openmct.objects.getOriginalPath(object.identifier);
|
||||||
|
|
||||||
|
const navigateToParent = '/browse/'
|
||||||
|
+ objectPath.slice(1)
|
||||||
|
.map(parent => this.openmct.objects.makeKeyString(parent.identifier))
|
||||||
|
.join('/');
|
||||||
|
|
||||||
|
const filteredChild = {
|
||||||
|
id: this.openmct.objects.makeKeyString(object.identifier),
|
||||||
|
object,
|
||||||
|
objectPath,
|
||||||
|
navigateToParent
|
||||||
|
};
|
||||||
|
|
||||||
|
this.filteredTreeItems.push(filteredChild);
|
||||||
|
}
|
||||||
|
},
|
||||||
searchTree(value) {
|
searchTree(value) {
|
||||||
this.searchValue = value;
|
this.searchValue = value;
|
||||||
|
this.isLoading = true;
|
||||||
|
|
||||||
if (this.searchValue !== '') {
|
if (this.searchValue !== '') {
|
||||||
this.getFilteredChildren();
|
this.getDebouncedFilteredChildren();
|
||||||
|
} else {
|
||||||
|
this.isLoading = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
handleItemSelection(item, node) {
|
handleItemSelection(item, node) {
|
||||||
|
@ -21,21 +21,22 @@
|
|||||||
*****************************************************************************/
|
*****************************************************************************/
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="c-style">
|
<div class="c-style has-local-controls c-toolbar">
|
||||||
<span :class="[
|
<div class="c-style__controls">
|
||||||
{ 'is-style-invisible': styleItem.style.isStyleInvisible },
|
<div :class="[
|
||||||
{ 'c-style-thumb--mixed': mixedStyles.indexOf('backgroundColor') > -1 }
|
{ '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"
|
: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) }"
|
|
||||||
>
|
>
|
||||||
ABC
|
<span class="c-style-thumb__text"
|
||||||
</span>
|
:class="{ 'hide-nice': !hasProperty(styleItem.style.color) }"
|
||||||
</span>
|
>
|
||||||
<span class="c-toolbar">
|
ABC
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<toolbar-color-picker v-if="hasProperty(styleItem.style.border)"
|
<toolbar-color-picker v-if="hasProperty(styleItem.style.border)"
|
||||||
class="c-style__toolbar-button--border-color u-menu-to--center"
|
class="c-style__toolbar-button--border-color u-menu-to--center"
|
||||||
:options="borderColorOption"
|
:options="borderColorOption"
|
||||||
@ -61,7 +62,14 @@
|
|||||||
:options="isStyleInvisibleOption"
|
:options="isStyleInvisibleOption"
|
||||||
@change="updateStyleValue"
|
@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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -80,12 +88,11 @@ export default {
|
|||||||
ToolbarColorPicker,
|
ToolbarColorPicker,
|
||||||
ToolbarToggleButton
|
ToolbarToggleButton
|
||||||
},
|
},
|
||||||
inject: [
|
inject: ['openmct'],
|
||||||
'openmct'
|
|
||||||
],
|
|
||||||
props: {
|
props: {
|
||||||
isEditing: {
|
isEditing: {
|
||||||
type: Boolean
|
type: Boolean,
|
||||||
|
required: true
|
||||||
},
|
},
|
||||||
mixedStyles: {
|
mixedStyles: {
|
||||||
type: Array,
|
type: Array,
|
||||||
@ -93,6 +100,10 @@ export default {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
nonSpecificFontProperties: {
|
||||||
|
type: Array,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
styleItem: {
|
styleItem: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: true
|
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: {
|
methods: {
|
||||||
@ -216,6 +236,9 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.$emit('persist', this.styleItem, item.property);
|
this.$emit('persist', this.styleItem, item.property);
|
||||||
|
},
|
||||||
|
saveItemStyle() {
|
||||||
|
this.$emit('save-style', this.itemStyle);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -31,6 +31,11 @@
|
|||||||
<div class="c-inspect-styles__header">
|
<div class="c-inspect-styles__header">
|
||||||
Object Style
|
Object Style
|
||||||
</div>
|
</div>
|
||||||
|
<FontStyleEditor
|
||||||
|
v-if="canStyleFont"
|
||||||
|
:font-style="consolidatedFontStyle"
|
||||||
|
@set-font-property="setFontProperty"
|
||||||
|
/>
|
||||||
<div class="c-inspect-styles__content">
|
<div class="c-inspect-styles__content">
|
||||||
<div v-if="staticStyle"
|
<div v-if="staticStyle"
|
||||||
class="c-inspect-styles__style"
|
class="c-inspect-styles__style"
|
||||||
@ -39,7 +44,9 @@
|
|||||||
:style-item="staticStyle"
|
:style-item="staticStyle"
|
||||||
:is-editing="allowEditing"
|
:is-editing="allowEditing"
|
||||||
:mixed-styles="mixedStyles"
|
:mixed-styles="mixedStyles"
|
||||||
|
:non-specific-font-properties="nonSpecificFontProperties"
|
||||||
@persist="updateStaticStyle"
|
@persist="updateStaticStyle"
|
||||||
|
@save-style="saveStyle"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
@ -58,10 +65,11 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="c-inspect-styles__content c-inspect-styles__condition-set">
|
<div class="c-inspect-styles__content c-inspect-styles__condition-set">
|
||||||
<a v-if="conditionSetDomainObject"
|
<a v-if="conditionSetDomainObject"
|
||||||
class="c-object-label icon-conditional"
|
class="c-object-label"
|
||||||
:href="navigateToPath"
|
:href="navigateToPath"
|
||||||
@click="navigateOrPreview"
|
@click="navigateOrPreview"
|
||||||
>
|
>
|
||||||
|
<span class="c-object-label__type-icon icon-conditional"></span>
|
||||||
<span class="c-object-label__name">{{ conditionSetDomainObject.name }}</span>
|
<span class="c-object-label__name">{{ conditionSetDomainObject.name }}</span>
|
||||||
</a>
|
</a>
|
||||||
<template v-if="allowEditing">
|
<template v-if="allowEditing">
|
||||||
@ -80,6 +88,12 @@
|
|||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<FontStyleEditor
|
||||||
|
v-if="canStyleFont"
|
||||||
|
:font-style="consolidatedFontStyle"
|
||||||
|
@set-font-property="setFontProperty"
|
||||||
|
/>
|
||||||
|
|
||||||
<div v-if="conditionsLoaded"
|
<div v-if="conditionsLoaded"
|
||||||
class="c-inspect-styles__conditions"
|
class="c-inspect-styles__conditions"
|
||||||
>
|
>
|
||||||
@ -97,8 +111,10 @@
|
|||||||
/>
|
/>
|
||||||
<style-editor class="c-inspect-styles__editor"
|
<style-editor class="c-inspect-styles__editor"
|
||||||
:style-item="conditionStyle"
|
:style-item="conditionStyle"
|
||||||
|
:non-specific-font-properties="nonSpecificFontProperties"
|
||||||
:is-editing="allowEditing"
|
:is-editing="allowEditing"
|
||||||
@persist="updateConditionalStyle"
|
@persist="updateConditionalStyle"
|
||||||
|
@save-style="saveStyle"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -108,6 +124,7 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|
||||||
|
import FontStyleEditor from '@/ui/inspector/styles/FontStyleEditor.vue';
|
||||||
import StyleEditor from "./StyleEditor.vue";
|
import StyleEditor from "./StyleEditor.vue";
|
||||||
import PreviewAction from "@/ui/preview/PreviewAction.js";
|
import PreviewAction from "@/ui/preview/PreviewAction.js";
|
||||||
import { getApplicableStylesForItem, getConsolidatedStyleValues, getConditionSetIdentifierForItem } from "@/plugins/condition/utils/styleUtils";
|
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 ConditionDescription from "@/plugins/condition/components/ConditionDescription.vue";
|
||||||
import Vue from '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 {
|
export default {
|
||||||
name: 'StylesView',
|
name: 'StylesView',
|
||||||
components: {
|
components: {
|
||||||
|
FontStyleEditor,
|
||||||
StyleEditor,
|
StyleEditor,
|
||||||
ConditionError,
|
ConditionError,
|
||||||
ConditionDescription
|
ConditionDescription
|
||||||
},
|
},
|
||||||
inject: [
|
inject: [
|
||||||
'openmct',
|
'openmct',
|
||||||
'selection'
|
'selection',
|
||||||
|
'stylesManager'
|
||||||
],
|
],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
@ -139,19 +170,80 @@ export default {
|
|||||||
conditionsLoaded: false,
|
conditionsLoaded: false,
|
||||||
navigateToPath: '',
|
navigateToPath: '',
|
||||||
selectedConditionId: '',
|
selectedConditionId: '',
|
||||||
locked: false
|
items: [],
|
||||||
|
domainObject: undefined,
|
||||||
|
consolidatedFontStyle: {}
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
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() {
|
allowEditing() {
|
||||||
return this.isEditing && !this.locked;
|
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() {
|
destroyed() {
|
||||||
this.removeListeners();
|
this.removeListeners();
|
||||||
|
this.openmct.editor.off('isEditing', this.setEditState);
|
||||||
|
this.stylesManager.off('styleSelected', this.applyStyleToSelection);
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.items = [];
|
|
||||||
this.previewAction = new PreviewAction(this.openmct);
|
this.previewAction = new PreviewAction(this.openmct);
|
||||||
this.isMultipleSelection = this.selection.length > 1;
|
this.isMultipleSelection = this.selection.length > 1;
|
||||||
this.getObjectsAndItemsFromSelection();
|
this.getObjectsAndItemsFromSelection();
|
||||||
@ -166,7 +258,10 @@ export default {
|
|||||||
this.initializeStaticStyle();
|
this.initializeStaticStyle();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.setConsolidatedFontStyle();
|
||||||
|
|
||||||
this.openmct.editor.on('isEditing', this.setEditState);
|
this.openmct.editor.on('isEditing', this.setEditState);
|
||||||
|
this.stylesManager.on('styleSelected', this.applyStyleToSelection);
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
getObjectStyles() {
|
getObjectStyles() {
|
||||||
@ -178,10 +273,10 @@ export default {
|
|||||||
}
|
}
|
||||||
} else if (this.items.length) {
|
} else if (this.items.length) {
|
||||||
const itemId = this.items[0].id;
|
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];
|
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;
|
objectStyles = this.domainObject.configuration.objectStyles;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -219,6 +314,18 @@ export default {
|
|||||||
isItemType(type, item) {
|
isItemType(type, item) {
|
||||||
return item && (item.type === type);
|
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) {
|
hasConditionalStyle(domainObject, layoutItem) {
|
||||||
const id = layoutItem ? layoutItem.id : undefined;
|
const id = layoutItem ? layoutItem.id : undefined;
|
||||||
|
|
||||||
@ -235,13 +342,8 @@ export default {
|
|||||||
this.selection.forEach((selectionItem) => {
|
this.selection.forEach((selectionItem) => {
|
||||||
const item = selectionItem[0].context.item;
|
const item = selectionItem[0].context.item;
|
||||||
const layoutItem = selectionItem[0].context.layoutItem;
|
const layoutItem = selectionItem[0].context.layoutItem;
|
||||||
const layoutDomainObject = selectionItem[0].context.item;
|
|
||||||
const isChildItem = selectionItem.length > 1;
|
const isChildItem = selectionItem.length > 1;
|
||||||
|
|
||||||
if (layoutDomainObject && layoutDomainObject.locked) {
|
|
||||||
this.locked = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isChildItem) {
|
if (!isChildItem) {
|
||||||
domainObject = item;
|
domainObject = item;
|
||||||
itemStyle = getApplicableStylesForItem(item);
|
itemStyle = getApplicableStylesForItem(item);
|
||||||
@ -251,7 +353,7 @@ export default {
|
|||||||
} else {
|
} else {
|
||||||
this.canHide = true;
|
this.canHide = true;
|
||||||
domainObject = selectionItem[1].context.item;
|
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);
|
subObjects.push(item);
|
||||||
itemStyle = getApplicableStylesForItem(item);
|
itemStyle = getApplicableStylesForItem(item);
|
||||||
if (this.hasConditionalStyle(item)) {
|
if (this.hasConditionalStyle(item)) {
|
||||||
@ -275,7 +377,7 @@ export default {
|
|||||||
const {styles, mixedStyles} = getConsolidatedStyleValues(itemInitialStyles);
|
const {styles, mixedStyles} = getConsolidatedStyleValues(itemInitialStyles);
|
||||||
this.initialStyles = styles;
|
this.initialStyles = styles;
|
||||||
this.mixedStyles = mixedStyles;
|
this.mixedStyles = mixedStyles;
|
||||||
|
// main layout
|
||||||
this.domainObject = domainObject;
|
this.domainObject = domainObject;
|
||||||
this.removeListeners();
|
this.removeListeners();
|
||||||
if (this.domainObject) {
|
if (this.domainObject) {
|
||||||
@ -298,6 +400,7 @@ export default {
|
|||||||
isKeyItemId(key) {
|
isKeyItemId(key) {
|
||||||
return (key !== 'styles')
|
return (key !== 'styles')
|
||||||
&& (key !== 'staticStyle')
|
&& (key !== 'staticStyle')
|
||||||
|
&& (key !== 'fontStyle')
|
||||||
&& (key !== 'defaultConditionId')
|
&& (key !== 'defaultConditionId')
|
||||||
&& (key !== 'selectedConditionId')
|
&& (key !== 'selectedConditionId')
|
||||||
&& (key !== 'conditionSetIdentifier');
|
&& (key !== 'conditionSetIdentifier');
|
||||||
@ -637,6 +740,124 @@ export default {
|
|||||||
},
|
},
|
||||||
persist(domainObject, style) {
|
persist(domainObject, style) {
|
||||||
this.openmct.objects.mutate(domainObject, 'configuration.objectStyles', 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 {
|
&__condition-set {
|
||||||
|
align-items: baseline;
|
||||||
|
border-bottom: 1px solid $colorInteriorBorder;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: center;
|
padding-bottom: $interiorMargin;
|
||||||
|
|
||||||
.c-object-label {
|
.c-object-label {
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
@ -53,7 +55,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__style,
|
&__style {
|
||||||
|
padding-bottom: $interiorMargin;
|
||||||
|
}
|
||||||
|
|
||||||
&__condition {
|
&__condition {
|
||||||
padding: $interiorMargin;
|
padding: $interiorMargin;
|
||||||
}
|
}
|
||||||
|
@ -22,6 +22,7 @@
|
|||||||
|
|
||||||
import { createOpenMct, resetApplicationState } from "utils/testing";
|
import { createOpenMct, resetApplicationState } from "utils/testing";
|
||||||
import ConditionPlugin from "./plugin";
|
import ConditionPlugin from "./plugin";
|
||||||
|
import stylesManager from '@/ui/inspector/styles/StylesManager';
|
||||||
import StylesView from "./components/inspector/StylesView.vue";
|
import StylesView from "./components/inspector/StylesView.vue";
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
import {getApplicableStylesForItem} from "./utils/styleUtils";
|
import {getApplicableStylesForItem} from "./utils/styleUtils";
|
||||||
@ -146,6 +147,8 @@ describe('the plugin', function () {
|
|||||||
let displayLayoutItem;
|
let displayLayoutItem;
|
||||||
let lineLayoutItem;
|
let lineLayoutItem;
|
||||||
let boxLayoutItem;
|
let boxLayoutItem;
|
||||||
|
let notCreatableObjectItem;
|
||||||
|
let notCreatableObject;
|
||||||
let selection;
|
let selection;
|
||||||
let component;
|
let component;
|
||||||
let styleViewComponentObject;
|
let styleViewComponentObject;
|
||||||
@ -264,6 +267,19 @@ describe('the plugin', function () {
|
|||||||
"stroke": "#717171",
|
"stroke": "#717171",
|
||||||
"type": "line-view",
|
"type": "line-view",
|
||||||
"id": "57d49a28-7863-43bd-9593-6570758916f0"
|
"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": [
|
"layoutGrid": [
|
||||||
@ -297,6 +313,52 @@ describe('the plugin', function () {
|
|||||||
"type": "box-view",
|
"type": "box-view",
|
||||||
"id": "89b88746-d325-487b-aec4-11b79afff9e8"
|
"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 = [
|
selection = [
|
||||||
[{
|
[{
|
||||||
context: {
|
context: {
|
||||||
@ -316,6 +378,19 @@ describe('the plugin', function () {
|
|||||||
"index": 0
|
"index": 0
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
context: {
|
||||||
|
item: displayLayoutItem,
|
||||||
|
"supportsMultiSelect": true
|
||||||
|
}
|
||||||
|
}],
|
||||||
|
[{
|
||||||
|
context: {
|
||||||
|
"item": notCreatableObject,
|
||||||
|
"layoutItem": notCreatableObjectItem,
|
||||||
|
"index": 2
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
context: {
|
context: {
|
||||||
item: displayLayoutItem,
|
item: displayLayoutItem,
|
||||||
@ -326,14 +401,15 @@ describe('the plugin', function () {
|
|||||||
let viewContainer = document.createElement('div');
|
let viewContainer = document.createElement('div');
|
||||||
child.append(viewContainer);
|
child.append(viewContainer);
|
||||||
component = new Vue({
|
component = new Vue({
|
||||||
provide: {
|
|
||||||
openmct: openmct,
|
|
||||||
selection: selection
|
|
||||||
},
|
|
||||||
el: viewContainer,
|
el: viewContainer,
|
||||||
components: {
|
components: {
|
||||||
StylesView
|
StylesView
|
||||||
},
|
},
|
||||||
|
provide: {
|
||||||
|
openmct: openmct,
|
||||||
|
selection: selection,
|
||||||
|
stylesManager
|
||||||
|
},
|
||||||
template: '<styles-view/>'
|
template: '<styles-view/>'
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -344,7 +420,7 @@ describe('the plugin', function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('initializes the items in the view', () => {
|
it('initializes the items in the view', () => {
|
||||||
expect(styleViewComponentObject.items.length).toBe(2);
|
expect(styleViewComponentObject.items.length).toBe(3);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('initializes conditional styles', () => {
|
it('initializes conditional styles', () => {
|
||||||
@ -363,7 +439,7 @@ describe('the plugin', function () {
|
|||||||
|
|
||||||
return Vue.nextTick().then(() => {
|
return Vue.nextTick().then(() => {
|
||||||
expect(styleViewComponentObject.domainObject.configuration.objectStyles).toBeDefined();
|
expect(styleViewComponentObject.domainObject.configuration.objectStyles).toBeDefined();
|
||||||
[boxLayoutItem, lineLayoutItem].forEach((item) => {
|
[boxLayoutItem, lineLayoutItem, notCreatableObjectItem].forEach((item) => {
|
||||||
const itemStyles = styleViewComponentObject.domainObject.configuration.objectStyles[item.id].styles;
|
const itemStyles = styleViewComponentObject.domainObject.configuration.objectStyles[item.id].styles;
|
||||||
expect(itemStyles.length).toBe(2);
|
expect(itemStyles.length).toBe(2);
|
||||||
const foundStyle = itemStyles.find((style) => {
|
const foundStyle = itemStyles.find((style) => {
|
||||||
@ -385,7 +461,7 @@ describe('the plugin', function () {
|
|||||||
|
|
||||||
return Vue.nextTick().then(() => {
|
return Vue.nextTick().then(() => {
|
||||||
expect(styleViewComponentObject.domainObject.configuration.objectStyles).toBeDefined();
|
expect(styleViewComponentObject.domainObject.configuration.objectStyles).toBeDefined();
|
||||||
[boxLayoutItem, lineLayoutItem].forEach((item) => {
|
[boxLayoutItem, lineLayoutItem, notCreatableObjectItem].forEach((item) => {
|
||||||
const itemStyle = styleViewComponentObject.domainObject.configuration.objectStyles[item.id].staticStyle;
|
const itemStyle = styleViewComponentObject.domainObject.configuration.objectStyles[item.id].staticStyle;
|
||||||
expect(itemStyle).toBeDefined();
|
expect(itemStyle).toBeDefined();
|
||||||
const applicableStyles = getApplicableStylesForItem(styleViewComponentObject.domainObject, item);
|
const applicableStyles = getApplicableStylesForItem(styleViewComponentObject.domainObject, item);
|
||||||
@ -467,7 +543,6 @@ describe('the plugin', function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should evaluate as stale when telemetry is not received in the allotted time', (done) => {
|
it('should evaluate as stale when telemetry is not received in the allotted time', (done) => {
|
||||||
|
|
||||||
let conditionMgr = new ConditionManager(conditionSetDomainObject, openmct);
|
let conditionMgr = new ConditionManager(conditionSetDomainObject, openmct);
|
||||||
conditionMgr.on('conditionSetResultUpdated', mockListener);
|
conditionMgr.on('conditionSetResultUpdated', mockListener);
|
||||||
conditionMgr.telemetryObjects = {
|
conditionMgr.telemetryObjects = {
|
||||||
@ -489,7 +564,7 @@ describe('the plugin', function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should not evaluate as stale when telemetry is received in the allotted time', (done) => {
|
it('should not evaluate as stale when telemetry is received in the allotted time', (done) => {
|
||||||
const date = Date.now();
|
const date = 1;
|
||||||
conditionSetDomainObject.configuration.conditionCollection[0].configuration.criteria[0].input = ["0.4"];
|
conditionSetDomainObject.configuration.conditionCollection[0].configuration.criteria[0].input = ["0.4"];
|
||||||
let conditionMgr = new ConditionManager(conditionSetDomainObject, openmct);
|
let conditionMgr = new ConditionManager(conditionSetDomainObject, openmct);
|
||||||
conditionMgr.on('conditionSetResultUpdated', mockListener);
|
conditionMgr.on('conditionSetResultUpdated', mockListener);
|
||||||
|
@ -22,7 +22,7 @@
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<component :is="urlDefined ? 'a' : 'span'"
|
<component :is="urlDefined ? 'a' : 'span'"
|
||||||
class="c-condition-widget"
|
class="c-condition-widget u-style-receiver js-style-receiver"
|
||||||
:href="urlDefined ? internalDomainObject.url : null"
|
:href="urlDefined ? internalDomainObject.url : null"
|
||||||
>
|
>
|
||||||
<div class="c-condition-widget__label">
|
<div class="c-condition-widget__label">
|
||||||
|
@ -56,17 +56,24 @@ define([
|
|||||||
return {
|
return {
|
||||||
show: function (element) {
|
show: function (element) {
|
||||||
component = new Vue({
|
component = new Vue({
|
||||||
provide: {
|
|
||||||
openmct,
|
|
||||||
objectPath
|
|
||||||
},
|
|
||||||
el: element,
|
el: element,
|
||||||
components: {
|
components: {
|
||||||
AlphanumericFormatView: AlphanumericFormatView.default
|
AlphanumericFormatView: AlphanumericFormatView.default
|
||||||
},
|
},
|
||||||
template: '<alphanumeric-format-view></alphanumeric-format-view>'
|
provide: {
|
||||||
|
openmct,
|
||||||
|
objectPath
|
||||||
|
},
|
||||||
|
template: '<alphanumeric-format-view ref="alphanumericFormatView"></alphanumeric-format-view>'
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
getViewContext() {
|
||||||
|
if (component) {
|
||||||
|
return component.$refs.alphanumericFormatView.getViewContext();
|
||||||
|
} else {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
},
|
||||||
destroy: function () {
|
destroy: function () {
|
||||||
component.$destroy();
|
component.$destroy();
|
||||||
component = undefined;
|
component = undefined;
|
||||||
|
38
src/plugins/displayLayout/CustomStringFormatter.js
Normal file
38
src/plugins/displayLayout/CustomStringFormatter.js
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import printj from 'printj';
|
||||||
|
|
||||||
|
export default class CustomStringFormatter {
|
||||||
|
constructor(openmct, valueMetadata, itemFormat) {
|
||||||
|
this.openmct = openmct;
|
||||||
|
|
||||||
|
this.itemFormat = itemFormat;
|
||||||
|
this.valueMetadata = valueMetadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
format(datum) {
|
||||||
|
if (!this.itemFormat) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.itemFormat.startsWith('&')) {
|
||||||
|
return printj.sprintf(this.itemFormat, datum[this.valueMetadata.key]);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const key = this.itemFormat.slice(1);
|
||||||
|
const customFormatter = this.openmct.telemetry.getFormatter(key);
|
||||||
|
if (!customFormatter) {
|
||||||
|
throw new Error('Custom Formatter not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
return customFormatter.format(datum[this.valueMetadata.key]);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
|
||||||
|
return datum[this.valueMetadata.key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setFormat(itemFormat) {
|
||||||
|
this.itemFormat = itemFormat;
|
||||||
|
}
|
||||||
|
}
|
82
src/plugins/displayLayout/CustomStringFormatterSpec.js
Normal file
82
src/plugins/displayLayout/CustomStringFormatterSpec.js
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
import CustomStringFormatter from './CustomStringFormatter';
|
||||||
|
import { createOpenMct, resetApplicationState } from 'utils/testing';
|
||||||
|
|
||||||
|
const CUSTOM_FORMATS = [
|
||||||
|
{
|
||||||
|
key: 'sclk',
|
||||||
|
format: (value) => 2 * value
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'lts',
|
||||||
|
format: (value) => 3 * value
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const valueMetadata = {
|
||||||
|
key: "sin",
|
||||||
|
name: "Sine",
|
||||||
|
unit: "Hz",
|
||||||
|
formatString: "%0.2f",
|
||||||
|
hints: {
|
||||||
|
range: 1,
|
||||||
|
priority: 3
|
||||||
|
},
|
||||||
|
source: "sin"
|
||||||
|
};
|
||||||
|
|
||||||
|
const datum = {
|
||||||
|
name: "1 Sine Wave Generator",
|
||||||
|
utc: 1603930354000,
|
||||||
|
yesterday: 1603843954000,
|
||||||
|
sin: 0.587785209686822,
|
||||||
|
cos: -0.8090170253297632
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('CustomStringFormatter', function () {
|
||||||
|
let element;
|
||||||
|
let child;
|
||||||
|
let openmct;
|
||||||
|
let customStringFormatter;
|
||||||
|
|
||||||
|
beforeEach((done) => {
|
||||||
|
openmct = createOpenMct();
|
||||||
|
|
||||||
|
element = document.createElement('div');
|
||||||
|
child = document.createElement('div');
|
||||||
|
element.appendChild(child);
|
||||||
|
CUSTOM_FORMATS.forEach(openmct.telemetry.addFormat.bind({openmct}));
|
||||||
|
openmct.on('start', done);
|
||||||
|
openmct.startHeadless();
|
||||||
|
|
||||||
|
spyOn(openmct.telemetry, 'getFormatter');
|
||||||
|
openmct.telemetry.getFormatter.and.callFake((key) => CUSTOM_FORMATS.find(d => d.key === key));
|
||||||
|
|
||||||
|
customStringFormatter = new CustomStringFormatter(openmct, valueMetadata);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
return resetApplicationState(openmct);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('adds custom format sclk', () => {
|
||||||
|
const format = openmct.telemetry.getFormatter('sclk');
|
||||||
|
expect(format.key).toEqual('sclk');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('adds custom format lts', () => {
|
||||||
|
const format = openmct.telemetry.getFormatter('lts');
|
||||||
|
expect(format.key).toEqual('lts');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns correct value for custom format sclk', () => {
|
||||||
|
customStringFormatter.setFormat('&sclk');
|
||||||
|
const value = customStringFormatter.format(datum, valueMetadata);
|
||||||
|
expect(datum.sin * 2).toEqual(value);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns correct value for custom format lts', () => {
|
||||||
|
customStringFormatter.setFormat('<s');
|
||||||
|
const value = customStringFormatter.format(datum, valueMetadata);
|
||||||
|
expect(datum.sin * 3).toEqual(value);
|
||||||
|
});
|
||||||
|
});
|
@ -73,7 +73,6 @@ define(['lodash'], function (_) {
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const VIEW_TYPES = {
|
const VIEW_TYPES = {
|
||||||
'telemetry-view': {
|
'telemetry-view': {
|
||||||
value: 'telemetry-view',
|
value: 'telemetry-view',
|
||||||
@ -96,7 +95,6 @@ define(['lodash'], function (_) {
|
|||||||
class: 'icon-tabular-realtime'
|
class: 'icon-tabular-realtime'
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const APPLICABLE_VIEWS = {
|
const APPLICABLE_VIEWS = {
|
||||||
'telemetry-view': [
|
'telemetry-view': [
|
||||||
VIEW_TYPES['telemetry.plot.overlay'],
|
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) {
|
function getTextButton(selectedParent, selection) {
|
||||||
return {
|
return {
|
||||||
control: "button",
|
control: "button",
|
||||||
@ -423,7 +398,7 @@ define(['lodash'], function (_) {
|
|||||||
property: function (selectionPath) {
|
property: function (selectionPath) {
|
||||||
return getPath(selectionPath);
|
return getPath(selectionPath);
|
||||||
},
|
},
|
||||||
icon: "icon-font",
|
icon: "icon-pencil",
|
||||||
title: "Edit text properties",
|
title: "Edit text properties",
|
||||||
dialog: DIALOG_FORM.text
|
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() {
|
function getSeparator() {
|
||||||
return {
|
return {
|
||||||
control: "separator"
|
control: "separator"
|
||||||
@ -637,7 +639,9 @@ define(['lodash'], function (_) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isMainLayoutSelected(selectedObjects[0])) {
|
if (isMainLayoutSelected(selectedObjects[0])) {
|
||||||
return [getAddButton(selectedObjects)];
|
return [
|
||||||
|
getToggleGridButton(selectedObjects),
|
||||||
|
getAddButton(selectedObjects)];
|
||||||
}
|
}
|
||||||
|
|
||||||
let toolbar = {
|
let toolbar = {
|
||||||
@ -649,11 +653,11 @@ define(['lodash'], function (_) {
|
|||||||
'display-mode': [],
|
'display-mode': [],
|
||||||
'telemetry-value': [],
|
'telemetry-value': [],
|
||||||
'style': [],
|
'style': [],
|
||||||
'text-style': [],
|
|
||||||
'position': [],
|
'position': [],
|
||||||
'duplicate': [],
|
'duplicate': [],
|
||||||
'unit-toggle': [],
|
'unit-toggle': [],
|
||||||
'remove': []
|
'remove': [],
|
||||||
|
'toggle-grid': []
|
||||||
};
|
};
|
||||||
|
|
||||||
selectedObjects.forEach(selectionPath => {
|
selectedObjects.forEach(selectionPath => {
|
||||||
@ -699,12 +703,6 @@ define(['lodash'], function (_) {
|
|||||||
toolbar['telemetry-value'] = [getTelemetryValueMenu(selectionPath, selectedObjects)];
|
toolbar['telemetry-value'] = [getTelemetryValueMenu(selectionPath, selectedObjects)];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (toolbar['text-style'].length === 0) {
|
|
||||||
toolbar['text-style'] = [
|
|
||||||
getTextSizeMenu(selectedParent, selectedObjects)
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (toolbar.position.length === 0) {
|
if (toolbar.position.length === 0) {
|
||||||
toolbar.position = [
|
toolbar.position = [
|
||||||
getStackOrder(selectedParent, selectionPath),
|
getStackOrder(selectedParent, selectionPath),
|
||||||
@ -730,12 +728,6 @@ define(['lodash'], function (_) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (layoutItem.type === 'text-view') {
|
} else if (layoutItem.type === 'text-view') {
|
||||||
if (toolbar['text-style'].length === 0) {
|
|
||||||
toolbar['text-style'] = [
|
|
||||||
getTextSizeMenu(selectedParent, selectedObjects)
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (toolbar.position.length === 0) {
|
if (toolbar.position.length === 0) {
|
||||||
toolbar.position = [
|
toolbar.position = [
|
||||||
getStackOrder(selectedParent, selectionPath),
|
getStackOrder(selectedParent, selectionPath),
|
||||||
@ -800,6 +792,10 @@ define(['lodash'], function (_) {
|
|||||||
if (toolbar.duplicate.length === 0) {
|
if (toolbar.duplicate.length === 0) {
|
||||||
toolbar.duplicate = [getDuplicateButton(selectedParent, selectionPath, selectedObjects)];
|
toolbar.duplicate = [getDuplicateButton(selectedParent, selectionPath, selectedObjects)];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (toolbar['toggle-grid'].length === 0) {
|
||||||
|
toolbar['toggle-grid'] = [getToggleGridButton(selectedObjects, selectionPath)];
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let toolbarArray = Object.values(toolbar);
|
let toolbarArray = Object.values(toolbar);
|
||||||
|
@ -56,6 +56,28 @@ define(function () {
|
|||||||
1
|
1
|
||||||
],
|
],
|
||||||
required: true
|
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
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
34
src/plugins/displayLayout/actions/CopyToClipboardAction.js
Normal file
34
src/plugins/displayLayout/actions/CopyToClipboardAction.js
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import clipboard from '@/utils/clipboard';
|
||||||
|
|
||||||
|
export default class CopyToClipboardAction {
|
||||||
|
constructor(openmct) {
|
||||||
|
this.openmct = openmct;
|
||||||
|
|
||||||
|
this.cssClass = 'icon-duplicate';
|
||||||
|
this.description = 'Copy value to clipboard';
|
||||||
|
this.group = "action";
|
||||||
|
this.key = 'copyToClipboard';
|
||||||
|
this.name = 'Copy to Clipboard';
|
||||||
|
this.priority = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
invoke(objectPath, view = {}) {
|
||||||
|
const viewContext = view.getViewContext && view.getViewContext();
|
||||||
|
const formattedValue = viewContext.formattedValueForCopy();
|
||||||
|
|
||||||
|
clipboard.updateClipboard(formattedValue)
|
||||||
|
.then(() => {
|
||||||
|
this.openmct.notifications.info(`Success : copied '${formattedValue}' to clipboard `);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
this.openmct.notifications.error(`Failed : to copy '${formattedValue}' to clipboard `);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
appliesTo(objectPath, view = {}) {
|
||||||
|
let viewContext = view.getViewContext && view.getViewContext();
|
||||||
|
|
||||||
|
return viewContext && viewContext.formattedValueForCopy
|
||||||
|
&& typeof viewContext.formattedValueForCopy === 'function';
|
||||||
|
}
|
||||||
|
}
|
@ -29,7 +29,7 @@
|
|||||||
@endMove="() => $emit('endMove')"
|
@endMove="() => $emit('endMove')"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="c-box-view"
|
class="c-box-view u-style-receiver js-style-receiver"
|
||||||
:class="[styleClass]"
|
:class="[styleClass]"
|
||||||
:style="style"
|
:style="style"
|
||||||
></div>
|
></div>
|
||||||
@ -51,11 +51,11 @@ export default {
|
|||||||
height: 5
|
height: 5
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
inject: ['openmct'],
|
|
||||||
components: {
|
components: {
|
||||||
LayoutFrame
|
LayoutFrame
|
||||||
},
|
},
|
||||||
mixins: [conditionalStylesMixin],
|
mixins: [conditionalStylesMixin],
|
||||||
|
inject: ['openmct'],
|
||||||
props: {
|
props: {
|
||||||
item: {
|
item: {
|
||||||
type: Object,
|
type: Object,
|
||||||
|
@ -22,7 +22,7 @@
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="l-layout"
|
class="l-layout u-style-receiver js-style-receiver"
|
||||||
:class="{
|
:class="{
|
||||||
'is-multi-selected': selectedLayoutItems.length > 1,
|
'is-multi-selected': selectedLayoutItems.length > 1,
|
||||||
'allow-editing': isEditing
|
'allow-editing': isEditing
|
||||||
@ -31,21 +31,19 @@
|
|||||||
@click.capture="bypassSelection"
|
@click.capture="bypassSelection"
|
||||||
@drop="handleDrop"
|
@drop="handleDrop"
|
||||||
>
|
>
|
||||||
<!-- Background grid -->
|
<display-layout-grid
|
||||||
<div
|
|
||||||
v-if="isEditing"
|
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
|
<div class="l-layout__dimensions-vals">
|
||||||
v-if="gridSize[0] >= 3"
|
{{ layoutDimensions[0] }},{{ layoutDimensions[1] }}
|
||||||
class="c-grid__x l-grid l-grid-x"
|
</div>
|
||||||
: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>
|
</div>
|
||||||
<component
|
<component
|
||||||
:is="item.type"
|
:is="item.type"
|
||||||
@ -81,6 +79,7 @@ import TextView from './TextView.vue';
|
|||||||
import LineView from './LineView.vue';
|
import LineView from './LineView.vue';
|
||||||
import ImageView from './ImageView.vue';
|
import ImageView from './ImageView.vue';
|
||||||
import EditMarquee from './EditMarquee.vue';
|
import EditMarquee from './EditMarquee.vue';
|
||||||
|
import DisplayLayoutGrid from './DisplayLayoutGrid.vue';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
|
|
||||||
const TELEMETRY_IDENTIFIER_FUNCTIONS = {
|
const TELEMETRY_IDENTIFIER_FUNCTIONS = {
|
||||||
@ -127,6 +126,7 @@ const DUPLICATE_OFFSET = 3;
|
|||||||
|
|
||||||
let components = ITEM_TYPE_VIEW_MAP;
|
let components = ITEM_TYPE_VIEW_MAP;
|
||||||
components['edit-marquee'] = EditMarquee;
|
components['edit-marquee'] = EditMarquee;
|
||||||
|
components['display-layout-grid'] = DisplayLayoutGrid;
|
||||||
|
|
||||||
function getItemDefinition(itemType, ...options) {
|
function getItemDefinition(itemType, ...options) {
|
||||||
let itemView = ITEM_TYPE_VIEW_MAP[itemType];
|
let itemView = ITEM_TYPE_VIEW_MAP[itemType];
|
||||||
@ -140,6 +140,7 @@ function getItemDefinition(itemType, ...options) {
|
|||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: components,
|
components: components,
|
||||||
|
inject: ['openmct', 'options', 'objectPath'],
|
||||||
props: {
|
props: {
|
||||||
domainObject: {
|
domainObject: {
|
||||||
type: Object,
|
type: Object,
|
||||||
@ -151,26 +152,41 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
let domainObject = JSON.parse(JSON.stringify(this.domainObject));
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
internalDomainObject: domainObject,
|
|
||||||
initSelectIndex: undefined,
|
initSelectIndex: undefined,
|
||||||
selection: []
|
selection: [],
|
||||||
|
showGrid: true
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
gridSize() {
|
gridSize() {
|
||||||
return this.internalDomainObject.configuration.layoutGrid;
|
return this.domainObject.configuration.layoutGrid;
|
||||||
},
|
},
|
||||||
layoutItems() {
|
layoutItems() {
|
||||||
return this.internalDomainObject.configuration.items;
|
return this.domainObject.configuration.items;
|
||||||
},
|
},
|
||||||
selectedLayoutItems() {
|
selectedLayoutItems() {
|
||||||
return this.layoutItems.filter(item => {
|
return this.layoutItems.filter(item => {
|
||||||
return this.itemIsInCurrentSelection(item);
|
return this.itemIsInCurrentSelection(item);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
layoutDimensions() {
|
||||||
|
return this.domainObject.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() {
|
showMarquee() {
|
||||||
let selectionPath = this.selection[0];
|
let selectionPath = this.selection[0];
|
||||||
let singleSelectedLine = this.selection.length === 1
|
let singleSelectedLine = this.selection.length === 1
|
||||||
@ -179,14 +195,17 @@ export default {
|
|||||||
return this.isEditing && selectionPath && selectionPath.length > 1 && !singleSelectedLine;
|
return this.isEditing && selectionPath && selectionPath.length > 1 && !singleSelectedLine;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
inject: ['openmct', 'options', 'objectPath'],
|
watch: {
|
||||||
|
isEditing(value) {
|
||||||
|
if (value) {
|
||||||
|
this.showGrid = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.unlisten = this.openmct.objects.observe(this.internalDomainObject, '*', function (obj) {
|
|
||||||
this.internalDomainObject = JSON.parse(JSON.stringify(obj));
|
|
||||||
}.bind(this));
|
|
||||||
this.openmct.selection.on('change', this.setSelection);
|
this.openmct.selection.on('change', this.setSelection);
|
||||||
this.initializeItems();
|
this.initializeItems();
|
||||||
this.composition = this.openmct.composition.get(this.internalDomainObject);
|
this.composition = this.openmct.composition.get(this.domainObject);
|
||||||
this.composition.on('add', this.addChild);
|
this.composition.on('add', this.addChild);
|
||||||
this.composition.on('remove', this.removeChild);
|
this.composition.on('remove', this.removeChild);
|
||||||
this.composition.load();
|
this.composition.load();
|
||||||
@ -195,7 +214,6 @@ export default {
|
|||||||
this.openmct.selection.off('change', this.setSelection);
|
this.openmct.selection.off('change', this.setSelection);
|
||||||
this.composition.off('add', this.addChild);
|
this.composition.off('add', this.addChild);
|
||||||
this.composition.off('remove', this.removeChild);
|
this.composition.off('remove', this.removeChild);
|
||||||
this.unlisten();
|
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
addElement(itemType, element) {
|
addElement(itemType, element) {
|
||||||
@ -322,7 +340,7 @@ export default {
|
|||||||
this.startingMinY2 = undefined;
|
this.startingMinY2 = undefined;
|
||||||
},
|
},
|
||||||
mutate(path, value) {
|
mutate(path, value) {
|
||||||
this.openmct.objects.mutate(this.internalDomainObject, path, value);
|
this.openmct.objects.mutate(this.domainObject, path, value);
|
||||||
},
|
},
|
||||||
handleDrop($event) {
|
handleDrop($event) {
|
||||||
if (!$event.dataTransfer.types.includes('openmct/domain-object-path')) {
|
if (!$event.dataTransfer.types.includes('openmct/domain-object-path')) {
|
||||||
@ -362,11 +380,11 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
containsObject(identifier) {
|
containsObject(identifier) {
|
||||||
return _.get(this.internalDomainObject, 'composition')
|
return _.get(this.domainObject, 'composition')
|
||||||
.some(childId => this.openmct.objects.areIdsEqual(childId, identifier));
|
.some(childId => this.openmct.objects.areIdsEqual(childId, identifier));
|
||||||
},
|
},
|
||||||
handleDragOver($event) {
|
handleDragOver($event) {
|
||||||
if (this.internalDomainObject.locked) {
|
if (this.domainObject.locked) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -395,7 +413,7 @@ export default {
|
|||||||
item.id = uuid();
|
item.id = uuid();
|
||||||
this.trackItem(item);
|
this.trackItem(item);
|
||||||
this.layoutItems.push(item);
|
this.layoutItems.push(item);
|
||||||
this.openmct.objects.mutate(this.internalDomainObject, "configuration.items", this.layoutItems);
|
this.openmct.objects.mutate(this.domainObject, "configuration.items", this.layoutItems);
|
||||||
this.initSelectIndex = this.layoutItems.length - 1;
|
this.initSelectIndex = this.layoutItems.length - 1;
|
||||||
},
|
},
|
||||||
trackItem(item) {
|
trackItem(item) {
|
||||||
@ -452,7 +470,7 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
removeFromComposition(keyString) {
|
removeFromComposition(keyString) {
|
||||||
let composition = _.get(this.internalDomainObject, 'composition');
|
let composition = _.get(this.domainObject, 'composition');
|
||||||
composition = composition.filter(identifier => {
|
composition = composition.filter(identifier => {
|
||||||
return this.openmct.objects.makeKeyString(identifier) !== keyString;
|
return this.openmct.objects.makeKeyString(identifier) !== keyString;
|
||||||
});
|
});
|
||||||
@ -604,10 +622,10 @@ export default {
|
|||||||
createNewDomainObject(domainObject, composition, viewType, nameExtension, model) {
|
createNewDomainObject(domainObject, composition, viewType, nameExtension, model) {
|
||||||
let identifier = {
|
let identifier = {
|
||||||
key: uuid(),
|
key: uuid(),
|
||||||
namespace: this.internalDomainObject.identifier.namespace
|
namespace: this.domainObject.identifier.namespace
|
||||||
};
|
};
|
||||||
let type = this.openmct.types.get(viewType);
|
let type = this.openmct.types.get(viewType);
|
||||||
let parentKeyString = this.openmct.objects.makeKeyString(this.internalDomainObject.identifier);
|
let parentKeyString = this.openmct.objects.makeKeyString(this.domainObject.identifier);
|
||||||
let objectName = nameExtension ? `${domainObject.name}-${nameExtension}` : domainObject.name;
|
let objectName = nameExtension ? `${domainObject.name}-${nameExtension}` : domainObject.name;
|
||||||
let object = {};
|
let object = {};
|
||||||
|
|
||||||
@ -664,7 +682,7 @@ export default {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
duplicateItem(selectedItems) {
|
duplicateItem(selectedItems) {
|
||||||
let objectStyles = this.internalDomainObject.configuration.objectStyles || {};
|
let objectStyles = this.domainObject.configuration.objectStyles || {};
|
||||||
let selectItemsArray = [];
|
let selectItemsArray = [];
|
||||||
let newDomainObjectsArray = [];
|
let newDomainObjectsArray = [];
|
||||||
|
|
||||||
@ -703,8 +721,8 @@ export default {
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
this.openmct.objects.mutate(this.internalDomainObject, "configuration.items", this.layoutItems);
|
this.openmct.objects.mutate(this.domainObject, "configuration.items", this.layoutItems);
|
||||||
this.openmct.objects.mutate(this.internalDomainObject, "configuration.objectStyles", objectStyles);
|
this.openmct.objects.mutate(this.domainObject, "configuration.objectStyles", objectStyles);
|
||||||
this.$el.click(); //clear selection;
|
this.$el.click(); //clear selection;
|
||||||
|
|
||||||
newDomainObjectsArray.forEach(domainObject => {
|
newDomainObjectsArray.forEach(domainObject => {
|
||||||
@ -743,13 +761,13 @@ export default {
|
|||||||
};
|
};
|
||||||
this.createNewDomainObject(mockDomainObject, overlayPlotIdentifiers, viewType).then((newDomainObject) => {
|
this.createNewDomainObject(mockDomainObject, overlayPlotIdentifiers, viewType).then((newDomainObject) => {
|
||||||
let newDomainObjectKeyString = this.openmct.objects.makeKeyString(newDomainObject.identifier);
|
let newDomainObjectKeyString = this.openmct.objects.makeKeyString(newDomainObject.identifier);
|
||||||
let internalDomainObjectKeyString = this.openmct.objects.makeKeyString(this.internalDomainObject.identifier);
|
let domainObjectKeyString = this.openmct.objects.makeKeyString(this.domainObject.identifier);
|
||||||
|
|
||||||
this.composition.add(newDomainObject);
|
this.composition.add(newDomainObject);
|
||||||
this.addItem('subobject-view', newDomainObject, position);
|
this.addItem('subobject-view', newDomainObject, position);
|
||||||
|
|
||||||
overlayPlots.forEach(overlayPlot => {
|
overlayPlots.forEach(overlayPlot => {
|
||||||
if (overlayPlot.location === internalDomainObjectKeyString) {
|
if (overlayPlot.location === domainObjectKeyString) {
|
||||||
this.openmct.objects.mutate(overlayPlot, 'location', newDomainObjectKeyString);
|
this.openmct.objects.mutate(overlayPlot, 'location', newDomainObjectKeyString);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -798,6 +816,9 @@ export default {
|
|||||||
|
|
||||||
this.removeItem(selection);
|
this.removeItem(selection);
|
||||||
this.initSelectIndex = this.layoutItems.length - 1; //restore 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>
|
@ -51,11 +51,11 @@ export default {
|
|||||||
url: element.url
|
url: element.url
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
inject: ['openmct'],
|
|
||||||
components: {
|
components: {
|
||||||
LayoutFrame
|
LayoutFrame
|
||||||
},
|
},
|
||||||
mixins: [conditionalStylesMixin],
|
mixins: [conditionalStylesMixin],
|
||||||
|
inject: ['openmct'],
|
||||||
props: {
|
props: {
|
||||||
item: {
|
item: {
|
||||||
type: Object,
|
type: Object,
|
||||||
@ -81,6 +81,7 @@ export default {
|
|||||||
style() {
|
style() {
|
||||||
let backgroundImage = 'url(' + this.item.url + ')';
|
let backgroundImage = 'url(' + this.item.url + ')';
|
||||||
let border = '1px solid ' + this.item.stroke;
|
let border = '1px solid ' + this.item.stroke;
|
||||||
|
|
||||||
if (this.itemStyle) {
|
if (this.itemStyle) {
|
||||||
if (this.itemStyle.imageUrl !== undefined) {
|
if (this.itemStyle.imageUrl !== undefined) {
|
||||||
backgroundImage = 'url(' + this.itemStyle.imageUrl + ')';
|
backgroundImage = 'url(' + this.itemStyle.imageUrl + ')';
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user