Compare commits

..

32 Commits

Author SHA1 Message Date
Jamie Vigliotta
c7da48d493 merged in new form factor code and updated link plugin 2021-06-03 17:44:29 -07:00
Mandlik, Nikhil K. (ARC-TI)[KBR Wyle Services, LLC]
4ba9bcb8f3 WIP: form refactor 2021-06-03 17:39:02 -07:00
Jamie Vigliotta
9fbacfd023 checking if possible parent is "locked" 2021-06-03 17:37:18 -07:00
Jamie Vigliotta
affa934766 fixed test 2021-06-03 17:37:18 -07:00
Jamie Vigliotta
f203806406 removed old link action legacy code, added new link action plugin 2021-06-03 17:37:18 -07:00
Mandlik, Nikhil K. (ARC-TI)[KBR Wyle Services, LLC]
6540518daa fixed duplicate action to work with new forms 2021-06-03 17:29:56 -07:00
Mandlik, Nikhil K. (ARC-TI)[KBR Wyle Services, LLC]
730efb75ed add pattern validation. 2021-06-03 17:29:56 -07:00
Mandlik, Nikhil K. (ARC-TI)[KBR Wyle Services, LLC]
a2aa0b8ab0 fixed logic to identify complex properties for creating and editing properties. 2021-06-03 17:29:56 -07:00
Mandlik, Nikhil K. (ARC-TI)[KBR Wyle Services, LLC]
8a3864edf6 fixed plan view to show plan file if exists. 2021-06-03 17:29:56 -07:00
Mandlik, Nikhil K. (ARC-TI)[KBR Wyle Services, LLC]
f1b748b46e added file input component 2021-06-03 17:29:56 -07:00
Mandlik, Nikhil K. (ARC-TI)[KBR Wyle Services, LLC]
a2a6d3020c fixed select field display value 2021-06-03 17:29:56 -07:00
Mandlik, Nikhil K. (ARC-TI)[KBR Wyle Services, LLC]
6813f2083a fixed Timer, default timerFormat value 2021-06-03 17:29:56 -07:00
Mandlik, Nikhil K. (ARC-TI)[KBR Wyle Services, LLC]
a641996a3f navigate to selected object in save in tree 2021-06-03 17:29:55 -07:00
Mandlik, Nikhil K. (ARC-TI)[KBR Wyle Services, LLC]
3cae138b6a cleanup 2021-06-03 17:29:55 -07:00
Mandlik, Nikhil K. (ARC-TI)[KBR Wyle Services, LLC]
a44d8ab451 fixed: If required field value is empty ok button should be disabled. 2021-06-03 17:29:55 -07:00
Mandlik, Nikhil K. (ARC-TI)[KBR Wyle Services, LLC]
af4db1f799 fixed autocomplete control field and issue with issue with form submit on autocomplete filed select via keyboard. 2021-06-03 17:29:55 -07:00
Mandlik, Nikhil K. (ARC-TI)[KBR Wyle Services, LLC]
38416b9434 small refactor and cleanup 2021-06-03 17:29:55 -07:00
Mandlik, Nikhil K. (ARC-TI)[KBR Wyle Services, LLC]
d27d4aba16 Fixed undefined name issue. 2021-06-03 17:29:55 -07:00
Mandlik, Nikhil K. (ARC-TI)[KBR Wyle Services, LLC]
354b590852 provide domain object to form validate and fixed validate functions. 2021-06-03 17:29:55 -07:00
Mandlik, Nikhil K. (ARC-TI)[KBR Wyle Services, LLC]
99427b4bed add required field validation + move form submit buttons from overlay to form itself. 2021-06-03 17:29:55 -07:00
Mandlik, Nikhil K. (ARC-TI)[KBR Wyle Services, LLC]
3b3b9368b5 add css class to form locator 2021-06-03 17:29:55 -07:00
Mandlik, Nikhil K. (ARC-TI)[KBR Wyle Services, LLC]
a829a14463 add default name value 2021-06-03 17:29:55 -07:00
Mandlik, Nikhil K. (ARC-TI)[KBR Wyle Services, LLC]
f42e37930c clean up + fixed small bug 2021-06-03 17:29:54 -07:00
Mandlik, Nikhil K. (ARC-TI)[KBR Wyle Services, LLC]
aeb9dbeb33 moved action specific operations into action files. 2021-06-03 17:29:54 -07:00
Mandlik, Nikhil K. (ARC-TI)[KBR Wyle Services, LLC]
f9393f80e0 fixed move action 2021-06-03 17:29:54 -07:00
Mandlik, Nikhil K. (ARC-TI)[KBR Wyle Services, LLC]
28fe4688db WIP for move action 2021-06-03 17:29:54 -07:00
Mandlik, Nikhil K. (ARC-TI)[KBR Wyle Services, LLC]
50192b1aca cleanup 2021-06-03 17:29:54 -07:00
Mandlik, Nikhil K. (ARC-TI)[KBR Wyle Services, LLC]
c5e3838353 WIP create new object 2021-06-03 17:29:54 -07:00
Mandlik, Nikhil K. (ARC-TI)[KBR Wyle Services, LLC]
a66e77821f fixed composite item value 2021-06-03 17:29:54 -07:00
Mandlik, Nikhil K. (ARC-TI)[KBR Wyle Services, LLC]
068d804a79 added textArea, update domainObject 2021-06-03 17:29:54 -07:00
Mandlik, Nikhil K. (ARC-TI)[KBR Wyle Services, LLC]
05558c02aa added changes from Shefali and updated form API. 2021-06-03 17:29:54 -07:00
Mandlik, Nikhil K. (ARC-TI)[KBR Wyle Services, LLC]
cc6c57398d WIP: form refactor 2021-06-03 17:29:51 -07:00
145 changed files with 2922 additions and 3984 deletions

View File

@@ -1,18 +0,0 @@
name: lighthouse
on:
workflow_dispatch:
inputs:
version:
description: 'Which branch do you want to test?' # Limited to branch for now
required: false
default: 'master'
jobs:
lighthouse:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
with:
ref: ${{ github.event.inputs.version }}
- uses: actions/setup-node@v1
- run: npm install && npm install -g @lhci/cli #Don't want to include this in our deps
- run: lhci autorun

3
.gitignore vendored
View File

@@ -40,7 +40,4 @@ npm-debug.log
# karma reports
report.*.json
# Lighthouse reports
.lighthouseci
package-lock.json

14
API.md
View File

@@ -595,17 +595,9 @@ section.
#### Limit Evaluators **draft**
Limit evaluators allow a telemetry integrator to define which limits exist for a
telemetry endpoint and how limits should be applied to telemetry from a given domain object.
A limit evaluator can implement the `evalute` method which is used to define how limits
should be applied to telemetry and the `getLimits` method which is used to specify
what the limit values are for different limit levels.
Limit levels can be mapped to one of 5 colors for visualization:
`purple`, `red`, `orange`, `yellow` and `cyan`.
For an example of a limit evaluator, take a look at `examples/generator/SinewaveLimitProvider.js`.
Limit evaluators allow a telemetry integrator to define how limits should be
applied to telemetry from a given domain object. For an example of a limit
evaluator, take a look at `examples/generator/SinewaveLimitProvider.js`.
### Telemetry Consumer APIs **draft**

View File

@@ -1,11 +1,9 @@
# Open MCT [![license](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](http://www.apache.org/licenses/LICENSE-2.0) [![Language grade: JavaScript](https://img.shields.io/lgtm/grade/javascript/g/nasa/openmct.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/nasa/openmct/context:javascript)
# Open MCT [![license](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](http://www.apache.org/licenses/LICENSE-2.0)
Open MCT (Open Mission Control Technologies) is a next-generation mission control framework for visualization of data on desktop and mobile devices. It is developed at NASA's Ames Research Center, and is being used by NASA for data analysis of spacecraft missions, as well as planning and operation of experimental rover systems. As a generalizable and open source framework, Open MCT could be used as the basis for building applications for planning, operation, and analysis of any systems producing telemetry data.
Please visit our [Official Site](https://nasa.github.io/openmct/) and [Getting Started Guide](https://nasa.github.io/openmct/getting-started/)
Once you've created something amazing with Open MCT, showcase your work in our GitHub Discussions [Show and Tell](https://github.com/nasa/openmct/discussions/categories/show-and-tell) section. We love seeing unique and wonderful implementations of Open MCT!
## See Open MCT in Action
Try Open MCT now with our [live demo](https://openmct-demo.herokuapp.com/).

View File

@@ -131,8 +131,7 @@ numbers by the following process:
3. In `package.json` change package to be public (private: false)
4. Test the package before publishing by doing `npm publish --dry-run`
if necessary.
5. Publish the package to the npmjs registry (e.g. `npm publish --access public`)
NOTE: Use the `--tag unstable` flag to the npm publishj if this is a prerelease.
5. Publish the package to the npmjs registry (e.g. `npm publish --access public`)
6. Confirm the package has been published (e.g. `https://www.npmjs.com/package/openmct`)
5. Update snapshot status in `package.json`
1. Create a new branch off the `master` branch.

View File

@@ -26,26 +26,14 @@ define([
) {
var PURPLE = {
sin: 2.2,
cos: 2.2
},
RED = {
var RED = {
sin: 0.9,
cos: 0.9
},
ORANGE = {
sin: 0.7,
cos: 0.7
},
YELLOW = {
sin: 0.5,
cos: 0.5
},
CYAN = {
sin: 0.45,
cos: 0.45
},
LIMITS = {
rh: {
cssClass: "is-limit--upr is-limit--red",
@@ -106,66 +94,32 @@ define([
};
SinewaveLimitProvider.prototype.getLimits = function (domainObject) {
return {
limits: function () {
return Promise.resolve({
WATCH: {
low: {
color: "cyan",
sin: -CYAN.sin,
cos: -CYAN.cos
},
high: {
color: "cyan",
...CYAN
}
},
return {
WARNING: {
low: {
color: "yellow",
cssClass: "is-limit--lwr is-limit--yellow",
sin: -YELLOW.sin,
cos: -YELLOW.cos
},
high: {
color: "yellow",
cssClass: "is-limit--upr is-limit--yellow",
...YELLOW
}
},
DISTRESS: {
low: {
color: "orange",
sin: -ORANGE.sin,
cos: -ORANGE.cos
},
high: {
color: "orange",
...ORANGE
}
},
CRITICAL: {
low: {
color: "red",
cssClass: "is-limit--lwr is-limit--red",
sin: -RED.sin,
cos: -RED.cos
},
high: {
color: "red",
cssClass: "is-limit--upr is-limit--red",
...RED
}
},
SEVERE: {
low: {
color: "purple",
sin: -PURPLE.sin,
cos: -PURPLE.cos
},
high: {
color: "purple",
...PURPLE
}
}
});
};
}
};
};

View File

@@ -194,7 +194,6 @@
['table', 'telemetry.plot.overlay', 'telemetry.plot.stacked'],
{indicator: true}
));
//openmct.install(openmct.plugins.CodeWalkthrough);
openmct.start();
</script>
</html>

View File

@@ -1,96 +0,0 @@
---
ci:
collect:
urls:
- http://localhost/
numberOfRuns: 5
settings:
onlyCategories:
- performance
- best-practices
upload:
target: temporary-public-storage
assert:
preset: lighthouse:recommended
assertions:
### Applicable assertions
bootup-time:
- warn
- minScore: 0.88 #Original value was calculated at 0.88
dom-size:
- error
- maxNumericValue: 200 #Original value was calculated at 188
first-contentful-paint:
- error
- minScore: 0.07 #Original value was calculated at 0.08
mainthread-work-breakdown:
- warn
- minScore: 0.8 #Original value was calculated at 0.8
unused-javascript:
- warn
- maxLength: 1
- error
- maxNumericValue: 2000 #Original value was calculated at 1855
unused-css-rules: warn
installable-manifest: warn
service-worker: warn
### Disabled seo, accessibility, and pwa assertions, below
categories:seo: 'off'
categories:accessibility: 'off'
categories:pwa: 'off'
accesskeys: 'off'
apple-touch-icon: 'off'
aria-allowed-attr: 'off'
aria-command-name: 'off'
aria-hidden-body: 'off'
aria-hidden-focus: 'off'
aria-input-field-name: 'off'
aria-meter-name: 'off'
aria-progressbar-name: 'off'
aria-required-attr: 'off'
aria-required-children: 'off'
aria-required-parent: 'off'
aria-roles: 'off'
aria-toggle-field-name: 'off'
aria-tooltip-name: 'off'
aria-treeitem-name: 'off'
aria-valid-attr: 'off'
aria-valid-attr-value: 'off'
button-name: 'off'
bypass: 'off'
canonical: 'off'
color-contrast: 'off'
content-width: 'off'
crawlable-anchors: 'off'
csp-xss: 'off'
font-display: 'off'
font-size: 'off'
maskable-icon: 'off'
heading-order: 'off'
hreflang: 'off'
html-has-lang: 'off'
html-lang-valid: 'off'
http-status-code: 'off'
image-alt: 'off'
input-image-alt: 'off'
is-crawlable: 'off'
label: 'off'
link-name: 'off'
link-text: 'off'
list: 'off'
listitem: 'off'
meta-description: 'off'
meta-refresh: 'off'
meta-viewport: 'off'
object-alt: 'off'
plugins: 'off'
robots-txt: 'off'
splash-screen: 'off'
tabindex: 'off'
tap-targets: 'off'
td-headers-attr: 'off'
th-has-data-cells: 'off'
themed-omnibox: 'off'
valid-lang: 'off'
video-caption: 'off'
viewport: 'off'

View File

@@ -1,6 +1,6 @@
{
"name": "openmct",
"version": "1.7.4",
"version": "1.7.3-SNAPSHOT",
"description": "The Open MCT core platform",
"dependencies": {},
"devDependencies": {
@@ -41,10 +41,10 @@
"jsdoc": "^3.3.2",
"karma": "5.1.1",
"karma-chrome-launcher": "3.1.0",
"karma-firefox-launcher": "1.3.0",
"karma-cli": "2.0.0",
"karma-coverage": "2.0.3",
"karma-coverage-istanbul-reporter": "3.0.3",
"karma-firefox-launcher": "1.3.0",
"karma-html-reporter": "0.2.7",
"karma-jasmine": "3.3.1",
"karma-sourcemap-loader": "0.3.7",
@@ -60,7 +60,7 @@
"moment-timezone": "0.5.28",
"node-bourbon": "^4.2.3",
"node-sass": "^4.14.1",
"painterro": "^1.2.56",
"painterro": "^1.0.35",
"printj": "^1.2.1",
"raw-loader": "^0.5.1",
"request": "^2.69.0",

View File

@@ -24,6 +24,7 @@ define([
"./src/navigation/NavigationService",
"./src/navigation/NavigateAction",
"./src/navigation/OrphanNavigationHandler",
"./src/windowing/NewTabAction",
"./res/templates/browse.html",
"./res/templates/browse-object.html",
"./res/templates/browse/object-header.html",
@@ -36,6 +37,7 @@ define([
NavigationService,
NavigateAction,
OrphanNavigationHandler,
NewTabAction,
browseTemplate,
browseObjectTemplate,
objectHeaderTemplate,
@@ -126,6 +128,23 @@ define([
"depends": [
"navigationService"
]
},
{
"key": "window",
"name": "Open In New Tab",
"implementation": NewTabAction,
"description": "Open in a new browser tab",
"category": [
"view-control",
"contextual"
],
"depends": [
"urlService",
"$window"
],
"group": "windowing",
"priority": 10,
"cssClass": "icon-new-window"
}
],
"runs": [

View File

@@ -20,29 +20,39 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
/**
* Module defining NewTabAction (Originally NewWindowAction). Created by vwoeltje on 11/18/14.
*/
define(
['./AbstractComposeAction'],
function (AbstractComposeAction) {
[],
function () {
/**
* The LinkAction is available from context menus and allows a user to
* link an object to another location of their choosing.
*
* @implements {Action}
* The new tab action allows a domain object to be opened
* into a new browser tab.
* @memberof platform/commonUI/browse
* @constructor
* @memberof platform/entanglement
* @implements {Action}
*/
function LinkAction(policyService, locationService, linkService, context) {
AbstractComposeAction.apply(
this,
[policyService, locationService, linkService, context, "Link"]
);
function NewTabAction(urlService, $window, context) {
context = context || {};
this.urlService = urlService;
this.open = function () {
arguments[0] += "&hideTree=true&hideInspector=true";
$window.open.apply($window, arguments);
};
// Choose the object to be opened into a new tab
this.domainObject = context.selectedObject || context.domainObject;
}
LinkAction.prototype = Object.create(AbstractComposeAction.prototype);
LinkAction.appliesTo = AbstractComposeAction.appliesTo;
NewTabAction.prototype.perform = function () {
this.open(
this.urlService.urlForNewTab("browse", this.domainObject),
"_blank"
);
};
return LinkAction;
return NewTabAction;
}
);

View File

@@ -0,0 +1,75 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, 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/windowing/NewTabAction"],
function (NewTabAction) {
describe("The new tab action", function () {
var actionSelected,
actionCurrent,
mockWindow,
mockContextCurrent,
mockContextSelected,
mockUrlService;
beforeEach(function () {
mockWindow = jasmine.createSpyObj("$window", ["open", "location"]);
// Context if the current object is selected
// For example, when the top right new tab
// button is clicked, the user is using the
// current domainObject
mockContextCurrent = jasmine.createSpyObj("context", ["domainObject"]);
// Context if the selected object is selected
// For example, when an object in the left
// tree is opened in a new tab using the
// context menu
mockContextSelected = jasmine.createSpyObj("context", ["selectedObject",
"domainObject"]);
// Mocks the urlService used to make the new tab's url from a
// domainObject and mode
mockUrlService = jasmine.createSpyObj("urlService", ["urlForNewTab"]);
// Action done using the current context or mockContextCurrent
actionCurrent = new NewTabAction(mockUrlService, mockWindow,
mockContextCurrent);
// Action done using the selected context or mockContextSelected
actionSelected = new NewTabAction(mockUrlService, mockWindow,
mockContextSelected);
});
it("new tab with current url is opened", function () {
actionCurrent.perform();
});
it("new tab with a selected url is opened", function () {
actionSelected.perform();
});
});
}
);

View File

@@ -20,7 +20,7 @@
at runtime from the About dialog for additional information.
-->
<mct-container key="c-overlay__contents">
<div class=c-overlay__top-bar">
<div class="c-overlay__top-bar">
<div class="c-overlay__dialog-title">{{ngModel.dialog.title}}</div>
<div class="c-overlay__dialog-hint hint">{{ngModel.dialog.hint}}</div>
</div>

View File

@@ -26,7 +26,6 @@ define([
"./src/controllers/EditObjectController",
"./src/actions/EditAndComposeAction",
"./src/actions/EditAction",
"./src/actions/PropertiesAction",
"./src/actions/SaveAction",
"./src/actions/SaveAndStopEditingAction",
"./src/actions/SaveAsAction",
@@ -55,7 +54,6 @@ define([
EditObjectController,
EditAndComposeAction,
EditAction,
PropertiesAction,
SaveAction,
SaveAndStopEditingAction,
SaveAsAction,
@@ -143,22 +141,6 @@ define([
"group": "action",
"priority": 10
},
{
"key": "properties",
"category": [
"contextual",
"view-control"
],
"implementation": PropertiesAction,
"cssClass": "major icon-pencil",
"name": "Edit Properties...",
"group": "action",
"priority": 10,
"description": "Edit properties of this object.",
"depends": [
"dialogService"
]
},
{
"key": "save-and-stop-editing",
"category": "save",

View File

@@ -21,11 +21,11 @@
*****************************************************************************/
define([
'../creation/CreateWizard',
// '../creation/CreateWizard',
'./SaveInProgressDialog'
],
function (
CreateWizard,
// CreateWizard,
SaveInProgressDialog
) {
@@ -100,7 +100,8 @@ function (
toUndirty = [];
function doWizardSave(parent) {
var wizard = self.createWizard(parent);
console.log('SaveAsAction');
// var wizard = self.createWizard(parent);
return self.dialogService
.getUserInput(wizard.getFormStructure(true),

View File

@@ -21,25 +21,21 @@
*****************************************************************************/
define([
"./src/actions/LinkAction",
"./src/actions/SetPrimaryLocationAction",
"./src/services/LocatingCreationDecorator",
"./src/services/LocatingObjectDecorator",
"./src/policies/CopyPolicy",
"./src/policies/CrossSpacePolicy",
"./src/capabilities/LocationCapability",
"./src/services/LinkService",
"./src/services/CopyService",
"./src/services/LocationService"
], function (
LinkAction,
SetPrimaryLocationAction,
LocatingCreationDecorator,
LocatingObjectDecorator,
CopyPolicy,
CrossSpacePolicy,
LocationCapability,
LinkService,
CopyService,
LocationService
) {
@@ -52,21 +48,6 @@ define([
"configuration": {},
"extensions": {
"actions": [
{
"key": "link",
"name": "Create Link",
"description": "Create Link to object in another location.",
"cssClass": "icon-link",
"category": "contextual",
"group": "action",
"priority": 7,
"implementation": LinkAction,
"depends": [
"policyService",
"locationService",
"linkService"
]
},
{
"key": "locate",
"name": "Set Primary Location",
@@ -115,15 +96,6 @@ define([
}
],
"services": [
{
"key": "linkService",
"name": "Link Service",
"description": "Provides a service for linking objects",
"implementation": LinkService,
"depends": [
"openmct"
]
},
{
"key": "copyService",
"name": "Copy Service",

View File

@@ -1,71 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, 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 () {
/**
* LinkService provides an interface for linking objects to additional
* locations. 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 LinkService(openmct) {
this.openmct = openmct;
}
LinkService.prototype.validate = function (object, parentCandidate) {
if (!parentCandidate || !parentCandidate.getId) {
return false;
}
if (parentCandidate.getId() === object.getId()) {
return false;
}
if (!parentCandidate.hasCapability('composition')) {
return false;
}
if (parentCandidate.getModel().composition.indexOf(object.getId()) !== -1) {
return false;
}
return this.openmct.composition.checkPolicy(parentCandidate.useCapability('adapter'), object.useCapability('adapter'));
};
LinkService.prototype.perform = function (object, parentObject) {
if (!this.validate(object, parentObject)) {
throw new Error(
"Tried to link objects without validating first."
);
}
return parentObject.getCapability('composition').add(object);
};
return LinkService;
}
);

View File

@@ -1,178 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, 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/LinkAction',
'../services/MockLinkService',
'../DomainObjectFactory'
],
function (LinkAction, MockLinkService, domainObjectFactory) {
describe("Link Action", function () {
var linkAction,
policyService,
locationService,
locationServicePromise,
linkService,
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);
linkService = new MockLinkService();
});
describe("with context from context-action", function () {
beforeEach(function () {
context = {
domainObject: selectedObject
};
linkAction = new LinkAction(
policyService,
locationService,
linkService,
context
);
});
it("initializes happily", function () {
expect(linkAction).toBeDefined();
});
describe("when performed it", function () {
beforeEach(function () {
linkAction.perform();
});
it("prompts for location", function () {
expect(locationService.getLocationFromUser)
.toHaveBeenCalledWith(
"Link selectedObject To a New Location",
"Link 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("links object to selected location", function () {
locationServicePromise
.then
.calls.mostRecent()
.args[0](newParent);
expect(linkService.perform)
.toHaveBeenCalledWith(selectedObject, newParent);
});
});
});
describe("with context from drag-drop", function () {
beforeEach(function () {
context = {
selectedObject: selectedObject,
domainObject: newParent
};
linkAction = new LinkAction(
policyService,
locationService,
linkService,
context
);
});
it("initializes happily", function () {
expect(linkAction).toBeDefined();
});
it("performs link immediately", function () {
linkAction.perform();
expect(linkService.perform)
.toHaveBeenCalledWith(selectedObject, newParent);
});
});
});
}
);

View File

@@ -1,215 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, 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/LinkService',
'../DomainObjectFactory',
'../ControlledPromise'
],
function (LinkService, domainObjectFactory, ControlledPromise) {
xdescribe("LinkService", function () {
var linkService,
mockPolicyService;
beforeEach(function () {
mockPolicyService = jasmine.createSpyObj(
'policyService',
['allow']
);
mockPolicyService.allow.and.returnValue(true);
linkService = new LinkService(mockPolicyService);
});
describe("validate", function () {
var object,
parentCandidate,
validate;
beforeEach(function () {
object = domainObjectFactory({
name: 'object'
});
parentCandidate = domainObjectFactory({
name: 'parentCandidate',
capabilities: {
composition: jasmine.createSpyObj(
'composition',
['invoke', 'add']
)
}
});
validate = function () {
return linkService.validate(object, parentCandidate);
};
});
it("does not allow invalid parentCandidate", function () {
parentCandidate = undefined;
expect(validate()).toBe(false);
parentCandidate = {};
expect(validate()).toBe(false);
});
it("does not allow parent to be object", function () {
parentCandidate.id = object.id = 'abc';
expect(validate()).toBe(false);
});
it("does not allow parent that contains object", function () {
object.id = 'abc';
parentCandidate.id = 'xyz';
parentCandidate.model.composition = ['abc'];
expect(validate()).toBe(false);
});
it("does not allow parents without composition", function () {
parentCandidate = domainObjectFactory({
name: 'parentCandidate'
});
object.id = 'abc';
parentCandidate.id = 'xyz';
parentCandidate.hasCapability.and.callFake(function (c) {
return c !== 'composition';
});
expect(validate()).toBe(false);
});
describe("defers to policyService", function () {
beforeEach(function () {
object.id = 'abc';
object.capabilities.type = { type: 'object' };
parentCandidate.id = 'xyz';
parentCandidate.capabilities.type = {
type: 'parentCandidate'
};
parentCandidate.model.composition = [];
});
it("calls policy service with correct args", function () {
validate();
expect(mockPolicyService.allow).toHaveBeenCalledWith(
"composition",
parentCandidate,
object
);
});
it("and returns false", function () {
mockPolicyService.allow.and.returnValue(true);
expect(validate()).toBe(true);
expect(mockPolicyService.allow).toHaveBeenCalled();
});
it("and returns true", function () {
mockPolicyService.allow.and.returnValue(false);
expect(validate()).toBe(false);
expect(mockPolicyService.allow).toHaveBeenCalled();
});
});
});
describe("perform", function () {
var object,
linkedObject,
parentModel,
parentObject,
compositionPromise,
addPromise,
compositionCapability;
beforeEach(function () {
compositionPromise = new ControlledPromise();
addPromise = new ControlledPromise();
compositionCapability = jasmine.createSpyObj(
'compositionCapability',
['invoke', 'add']
);
compositionCapability.invoke.and.returnValue(compositionPromise);
compositionCapability.add.and.returnValue(addPromise);
parentModel = {
composition: []
};
parentObject = domainObjectFactory({
name: 'parentObject',
model: parentModel,
capabilities: {
mutation: {
invoke: function (mutator) {
mutator(parentModel);
return new ControlledPromise();
}
},
composition: compositionCapability
}
});
object = domainObjectFactory({
name: 'object',
id: 'xyz'
});
linkedObject = domainObjectFactory({
name: 'object-link',
id: 'xyz'
});
});
it("adds to the parent's composition", function () {
expect(compositionCapability.add).not.toHaveBeenCalled();
linkService.perform(object, parentObject);
expect(compositionCapability.add)
.toHaveBeenCalledWith(object);
});
it("returns object representing new link", function () {
var returnPromise, whenComplete;
returnPromise = linkService.perform(object, parentObject);
whenComplete = jasmine.createSpy('whenComplete');
returnPromise.then(whenComplete);
addPromise.resolve(linkedObject);
compositionPromise.resolve([linkedObject]);
expect(whenComplete).toHaveBeenCalledWith(linkedObject);
});
it("throws an error when performed on invalid inputs", function () {
function perform() {
linkService.perform(object, parentObject);
}
spyOn(linkService, 'validate');
linkService.validate.and.returnValue(true);
expect(perform).not.toThrow();
linkService.validate.and.returnValue(false);
expect(perform).toThrow();
});
});
});
}
);

View File

@@ -282,7 +282,7 @@ define([
}
],
"model": {
"timerFormat": "DDD hh:mm:ss"
"timerFormat": "long"
}
}
],

View File

@@ -47,6 +47,7 @@ define([
'./plugins/licenses/plugin',
'./plugins/remove/plugin',
'./plugins/move/plugin',
'./plugins/linkAction/plugin',
'./plugins/duplicate/plugin',
'vue'
], function (
@@ -76,6 +77,7 @@ define([
LicensesPlugin,
RemoveActionPlugin,
MoveActionPlugin,
LinkActionPlugin,
DuplicateActionPlugin,
Vue
) {
@@ -253,6 +255,7 @@ define([
this.status = new api.StatusAPI(this);
this.router = new ApplicationRouter(this);
this.forms = new api.FormsAPI.default(this);
this.branding = BrandingAPI.default;
@@ -268,13 +271,13 @@ define([
this.install(LicensesPlugin.default());
this.install(RemoveActionPlugin.default());
this.install(MoveActionPlugin.default());
this.install(LinkActionPlugin.default());
this.install(DuplicateActionPlugin.default());
this.install(this.plugins.FolderView());
this.install(this.plugins.Tabs());
this.install(ImageryPlugin.default());
this.install(this.plugins.FlexibleLayout());
this.install(this.plugins.GoToOriginalAction());
this.install(this.plugins.OpenInNewTabAction());
this.install(this.plugins.ImportExport());
this.install(this.plugins.WebPage());
this.install(this.plugins.Condition());

View File

@@ -21,7 +21,7 @@
*****************************************************************************/
import _ from 'lodash';
const INSIDE_EDIT_PATH_BLACKLIST = ["copy", "follow", "link", "locate", "move", "link"];
const OUTSIDE_EDIT_PATH_BLACKLIST = ["copy", "follow", "properties", "move", "link", "remove", "locate"];
const OUTSIDE_EDIT_PATH_BLACKLIST = ["copy", "follow", "move", "link", "remove", "locate"];
export default class LegacyContextMenuAction {
constructor(openmct, LegacyAction) {

View File

@@ -48,12 +48,12 @@ define(
* Converts an HTML element into a PNG or JPG Blob.
* @private
* @param {node} element that will be converted to an image
* @param {object} options Image options.
* @param {string} type of image to convert the element to.
* @returns {promise}
*/
ExportImageService.prototype.renderElement = function (element, {imageType, className, thumbnailSize}) {
const self = this;
ExportImageService.prototype.renderElement = function (element, imageType, className) {
const dialogService = this.dialogService;
const dialog = dialogService.showBlockingMessage({
title: "Capturing...",
hint: "Capturing an image",
@@ -90,16 +90,7 @@ define(
dialog.dismiss();
return new Promise(function (resolve, reject) {
if (thumbnailSize) {
const thumbnail = self.getThumbnail(canvas, mimeType, thumbnailSize);
return canvas.toBlob(blob => resolve({
blob,
thumbnail
}), mimeType);
}
return canvas.toBlob(blob => resolve({ blob }), mimeType);
return canvas.toBlob(resolve, mimeType);
});
}, function (error) {
console.log('error capturing image', error);
@@ -118,17 +109,6 @@ define(
});
};
ExportImageService.prototype.getThumbnail = function (canvas, mimeType, size) {
const thumbnailCanvas = document.createElement('canvas');
thumbnailCanvas.setAttribute('width', size.width);
thumbnailCanvas.setAttribute('height', size.height);
const ctx = thumbnailCanvas.getContext('2d');
ctx.globalCompositeOperation = "copy";
ctx.drawImage(canvas, 0, 0, canvas.width, canvas.height, 0, 0, size.width, size.height);
return thumbnailCanvas.toDataURL(mimeType);
};
/**
* Takes a screenshot of a DOM node and exports to JPG.
* @param {node} element to be exported
@@ -139,13 +119,9 @@ define(
ExportImageService.prototype.exportJPG = function (element, filename, className) {
const processedFilename = replaceDotsWithUnderscores(filename);
return this.renderElement(element, {
imageType: 'jpg',
className
})
.then(function (img) {
saveAs(img.blob, processedFilename);
});
return this.renderElement(element, "jpg", className).then(function (img) {
saveAs(img, processedFilename);
});
};
/**
@@ -158,13 +134,9 @@ define(
ExportImageService.prototype.exportPNG = function (element, filename, className) {
const processedFilename = replaceDotsWithUnderscores(filename);
return this.renderElement(element, {
imageType: 'png',
className
})
.then(function (img) {
saveAs(img.blob, processedFilename);
});
return this.renderElement(element, "png", className).then(function (img) {
saveAs(img, processedFilename);
});
};
/**
@@ -174,12 +146,8 @@ define(
* @returns {promise}
*/
ExportImageService.prototype.exportPNGtoSRC = function (element, options) {
return this.renderElement(element, {
imageType: 'png',
...options
});
ExportImageService.prototype.exportPNGtoSRC = function (element, className) {
return this.renderElement(element, "png", className);
};
function replaceDotsWithUnderscores(filename) {

View File

@@ -21,41 +21,44 @@
*****************************************************************************/
define([
'./time/TimeAPI',
'./objects/ObjectAPI',
'./composition/CompositionAPI',
'./types/TypeRegistry',
'./telemetry/TelemetryAPI',
'./indicators/IndicatorAPI',
'./notifications/NotificationAPI',
'./Editor',
'./menu/MenuAPI',
'./actions/ActionsAPI',
'./status/StatusAPI'
'./composition/CompositionAPI',
'./Editor',
'./forms/FormsAPI',
'./indicators/IndicatorAPI',
'./menu/MenuAPI',
'./notifications/NotificationAPI',
'./objects/ObjectAPI',
'./status/StatusAPI',
'./telemetry/TelemetryAPI',
'./time/TimeAPI',
'./types/TypeRegistry'
], function (
TimeAPI,
ObjectAPI,
CompositionAPI,
TypeRegistry,
TelemetryAPI,
IndicatorAPI,
NotificationAPI,
EditorAPI,
MenuAPI,
ActionsAPI,
StatusAPI
CompositionAPI,
EditorAPI,
FormsAPI,
IndicatorAPI,
MenuAPI,
NotificationAPI,
ObjectAPI,
StatusAPI,
TelemetryAPI,
TimeAPI,
TypeRegistry
) {
return {
TimeAPI: TimeAPI,
ObjectAPI: ObjectAPI,
CompositionAPI: CompositionAPI,
TypeRegistry: TypeRegistry,
TelemetryAPI: TelemetryAPI,
IndicatorAPI: IndicatorAPI,
NotificationAPI: NotificationAPI.default,
EditorAPI: EditorAPI,
MenuAPI: MenuAPI.default,
ActionsAPI: ActionsAPI.default,
StatusAPI: StatusAPI.default
CompositionAPI: CompositionAPI,
EditorAPI: EditorAPI,
FormsAPI: FormsAPI,
IndicatorAPI: IndicatorAPI,
MenuAPI: MenuAPI.default,
NotificationAPI: NotificationAPI.default,
ObjectAPI: ObjectAPI,
StatusAPI: StatusAPI.default,
TelemetryAPI: TelemetryAPI,
TimeAPI: TimeAPI,
TypeRegistry: TypeRegistry,
};
});

View File

@@ -0,0 +1,135 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, 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 CreateWizard {
constructor(openmct, domainObject, parent) {
this.openmct = openmct;
this.domainObject = domainObject;
this.type = openmct.types.get(domainObject.type);
this.model = domainObject;
this.parent = parent;
this.properties = this.type.definition.form || [];
}
addNotes(sections) {
const row = {
control: 'textarea',
cssClass: 'l-textarea-sm',
key: 'notes',
name: 'Notes',
required: false,
value: this.domainObject.notes
};
sections.forEach(section => {
if (section.name !== 'Properties') {
return;
}
section.rows.unshift(row);
});
}
addTitle(sections) {
const row = {
control: 'textfield',
cssClass: 'l-input-lg',
key: 'name',
name: 'Title',
pattern: `\\S+`,
required: true,
value: this.domainObject.name
};
sections.forEach(section => {
if (section.name !== 'Properties') {
return;
}
section.rows.unshift(row);
});
}
/**
* Get the form model for this wizard; this is a description
* that will be rendered to an HTML form. See the
* platform/forms bundle
* @param {boolean} includeLocation if true, a 'location' section
* will be included that will allow the user to select the location
* of the newly created object, otherwise the .location property of
* the model will be used.
*/
getFormStructure(includeLocation) {
let sections = [];
let domainObject = this.domainObject;
let self = this;
sections.push({
name: 'Properties',
rows: this.properties.map(property => {
const row = JSON.parse(JSON.stringify(property));
row.value = this.getValue(row);
return row;
}).filter(row => row && row.control)
});
this.addNotes(sections);
this.addTitle(sections);
// Ensure there is always a 'save in' section
if (includeLocation) {
function validateLocation(object, data) {
return self.openmct.composition.checkPolicy(data.value, object);
}
sections.push({
name: 'Location',
cssClass: 'grows',
rows: [{
name: 'Save In',
cssClass: 'grows',
control: 'locator',
domainObject,
required: true,
parent: this.parent,
validate: validateLocation.bind(this),
key: 'location'
}]
});
}
return {
sections
};
}
getValue(row) {
if (row.property) {
return row.property.reduce((acc, property) => acc && acc[property], this.domainObject);
} else {
return this.domainObject[row.key];
}
}
}

159
src/api/forms/FormsAPI.js Normal file
View File

@@ -0,0 +1,159 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, 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 EditPropertiesAction from './actions/EditPropertiesAction';
import FormProperties from './components/FormProperties.vue';
import Vue from 'vue';
export const CONTROLS = [
"autocomplete",
"button",
"checkbox",
"color",
"composite",
"datetime",
"dialog-button",
"file-input",
"menu-button",
"numberfield",
"radio",
"select",
"textarea",
"textfield"
];
export default class FormsAPI {
constructor(openmct) {
this.openmct = openmct;
this.controls = {};
this.init();
}
addControl(name, actions) {
const control = this.controls[name];
if (control) {
this.openmct.notifications.error(`Error: provided form control '${name}', already exists`);
return;
}
this.controls[name] = actions;
}
getAllControls() {
return this.controls;
}
getControl(name) {
const control = this.controls[name];
if (control) {
console.error(`Error: form control '${name}', does not exist`);
}
return control;
}
showForm(formStructure, options) {
const changes = {};
let overlay;
let parentDomainObject = options.parentDomainObject || {};
const domainObject = options.domainObject;
const onSave = () => {
overlay.dismiss();
if(options.onSave) {
options.onSave(domainObject, changes, parentDomainObject);
}
};
const onDismiss = () => {
overlay.dismiss();
if (options.onDismiss) {
options.onDismiss();
};
};
const vm = new Vue({
components: { FormProperties },
provide: {
openmct: this.openmct,
domainObject
},
data() {
return {
formStructure,
onChange,
onDismiss,
onSave
};
},
template: '<FormProperties :model="formStructure" @onChange="onChange" @onDismiss="onDismiss" @onSave="onSave"></FormProperties>'
}).$mount();
overlay = this.openmct.overlays.overlay({
element: vm.$el,
size: 'small',
onDestroy: () => vm.$destroy()
});
function onChange(data) {
if (options.onChange) {
options.onChange(data);
}
if (data.model) {
const property = data.model.property;
let key = data.model.key;
if (key === 'location') {
parentDomainObject = data.value;
}
if (property && property.length) {
key = property.join('.');
}
changes[key] = data.value;
}
}
}
// Private methods
/**
* @private
*/
_addDefaultFormControls() {
CONTROLS.forEach(control => {
this.addControl(control);
});
}
// Init
init() {
this.openmct.actions.register(new EditPropertiesAction(this.openmct));
this._addDefaultFormControls();
}
}

View File

@@ -0,0 +1,131 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, 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 PropertiesAction from './PropertiesAction';
import CreateWizard from '../CreateWizard';
import uuid from 'uuid';
export default class CreateAction extends PropertiesAction {
constructor(openmct, type, parentDomainObject) {
super(openmct);
this.type = type;
this.parentDomainObject = parentDomainObject;
}
invoke() {
this._showCreateForm(this.type);
}
// Private methods
async _onSave(domainObject, changes, parentDomainObject) {
Object.entries(changes).forEach(([key, value]) => {
const properties = key.split('.');
let object = this.domainObject;
const propertiesLength = properties.length;
properties.forEach((property, index) => {
const isComplexProperty = propertiesLength > 1 && index != propertiesLength - 1;
if (isComplexProperty && object[property] !== null) {
object = object[property];
} else {
object[property] = value;
}
});
object = value;
});
this.domainObject.modified = Date.now();
this.domainObject.location = this.openmct.objects.makeKeyString(parentDomainObject.identifier);
this.domainObject.identifier.namespace = parentDomainObject.identifier.namespace;
// Show saving progress dialog
let dialog = this.openmct.overlays.progressDialog({
progressPerc: 'unknown',
message: 'Do not navigate away from this page or close this browser tab while this message is displayed.',
iconClass: 'info',
title: 'Saving'
});
const success = await this.openmct.objects.save(this.domainObject);
if (success) {
const compositionCollection = await openmct.composition.get(parentDomainObject);
compositionCollection.add(this.domainObject);
this._navigateAndEdit(this.domainObject);
this.openmct.notifications.info('Save successful');
} else {
this.openmct.notifications.error('Error saving objects');
console.error(error);
}
dialog.dismiss();
}
async _navigateAndEdit(domainObject) {
const objectPath = await this.openmct.objects.getOriginalPath(domainObject.identifier);
const url = '#/browse/' + objectPath
.map(object => object && this.openmct.objects.makeKeyString(object.identifier.key))
.reverse()
.join('/');
window.location.href = url;
const objectView = openmct.objectViews.get(domainObject, objectPath)[0];
const canEdit = objectView && objectView.canEdit && objectView.canEdit(domainObject, objectPath);
if (canEdit) {
openmct.editor.edit();
}
}
_showCreateForm(type) {
const typeDefinition = this.openmct.types.get(type);
const definition = typeDefinition.definition;
const domainObject = {
name: `Unnamed ${definition.name}`,
type,
identifier: {
key: uuid(),
namespace: this.parentDomainObject.identifier.namespace
}
};
this.domainObject = domainObject;
if (definition.initialize) {
definition.initialize(domainObject);
}
const createWizard = new CreateWizard(this.openmct, domainObject, this.parentDomainObject);
const formStructure = createWizard.getFormStructure(true);
formStructure.title = 'Create a New ' + definition.name;
const options = {
domainObject,
onSave: this._onSave.bind(this)
};
this.openmct.forms.showForm(formStructure, options);
}
}

View File

@@ -0,0 +1,102 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, 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 PropertiesAction from './PropertiesAction';
import CreateWizard from '../CreateWizard';
export default class EditPropertiesAction extends PropertiesAction {
constructor(openmct) {
super(openmct);
this.name = 'Edit Properties...';
this.key = 'properties';
this.description = 'Edit properties of this object.';
this.cssClass = 'major icon-pencil';
this.hideInDefaultMenu = true;
this.group = 'action';
this.priority = 10;
this.formProperties = {}
}
appliesTo(objectPath) {
const definition = this._getTypeDefinition(objectPath[0].type);
return definition && definition.creatable;
}
invoke(objectPath) {
this._showEditForm(objectPath);
}
// Private methods
async _onSave(domainObject, changes, parentDomainObject) {
Object.entries(changes).forEach(([key, value]) => {
const properties = key.split('.');
let object = this.domainObject;
const propertiesLength = properties.length;
properties.forEach((property, index) => {
const isComplexProperty = propertiesLength > 1 && index != propertiesLength - 1;
if (isComplexProperty && object[property] !== null) {
object = object[property];
} else {
object[property] = value;
}
});
object = value;
});
this.domainObject.modified = Date.now();
// Show saving progress dialog
let dialog = this.openmct.overlays.progressDialog({
progressPerc: 'unknown',
message: 'Do not navigate away from this page or close this browser tab while this message is displayed.',
iconClass: 'info',
title: 'Saving'
});
const success = await this.openmct.objects.save(this.domainObject);
if (success) {
this.openmct.notifications.info('Save successful');
} else {
this.openmct.notifications.error('Error saving objects');
console.error(error);
}
dialog.dismiss();
}
_showEditForm(objectPath) {
this.domainObject = objectPath[0];
const createWizard = new CreateWizard(this.openmct, this.domainObject, objectPath[1]);
const formStructure = createWizard.getFormStructure(false);
formStructure.title = 'Edit ' + this.domainObject.name;
const options = {
domainObject: this.domainObject,
onSave: this._onSave.bind(this)
};
this.openmct.forms.showForm(formStructure, options);
}
}

View File

@@ -4,12 +4,12 @@
* 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.
* '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
* 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.
@@ -19,20 +19,17 @@
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import objectPathToUrl from '/src/tools/url';
export default class OpenInNewTab {
export default class PropertiesAction {
constructor(openmct) {
this.name = 'Open In New Tab';
this.key = 'newTab';
this.description = 'Open in a new browser tab';
this.group = "windowing";
this.priority = 10;
this.cssClass = "icon-new-window";
this._openmct = openmct;
this.openmct = openmct;
}
invoke(objectPath) {
let url = objectPathToUrl(this._openmct, objectPath);
window.open(url);
/**
* @private
*/
_getTypeDefinition(type) {
const TypeDefinition = this.openmct.types.get(type);
return TypeDefinition.definition;
}
}

View File

@@ -0,0 +1,99 @@
<template>
<div>
<form name="mctForm"
class="form c-form mct-form"
autocomplete="off"
@submit.prevent
>
<div class="mct-form__title c-overlay__top-bar">
<div class="c-overlay__dialog-title">{{ model.title }}</div>
<div class="c-overlay__dialog-hint hint">All fields marked <span class="req icon-asterisk"></span> are required.</div>
</div>
<span v-for="section in model.sections"
:key="section.name"
class="mct-form__sections l-form-section c-form__section"
:class="section.cssClass"
>
<h2 class="c-form__header"
v-if="section.name"
>
{{ section.name }}
</h2>
<div v-for="(row, index) in section.rows"
:key="row.name"
>
<FormRow :css-class="section.cssClass"
:first="index < 1"
:row="row"
@onChange="onChange"
/>
</div>
</span>
</form>
<div class="mct-form__controls c-overlay__button-bar">
<button tabindex="0"
:disabled="isInvalid"
class="c-button c-button--major"
@click="onSave"
>
OK
</button>
<button tabindex="0"
class="c-button"
@click="onDismiss"
>
Cancel
</button>
</div>
</div>
</template>
<script>
import FormRow from "@/api/forms/components/FormRow.vue";
export default {
components: {
FormRow
},
inject: ['openmct', 'domainObject'],
props: {
model: {
type: Object,
required: true
},
value: {
type: Object,
required: false,
default() {
return {};
}
}
},
data() {
return {
inValidProperties: {}
};
},
computed: {
isInvalid() {
return Object.entries(this.inValidProperties)
.some(([key, value]) => {
return value;
});
}
},
methods: {
onChange(data) {
this.$set(this.inValidProperties, data.model.key, data.invalid);
this.$emit('onChange', data);
},
onDismiss() {
this.$emit('onDismiss');
},
onSave() {
this.$emit('onSave');
}
}
};
</script>

View File

@@ -0,0 +1,156 @@
<template>
<div class="form-row c-form__row"
:class="rowClass"
@onChange="onChange"
>
<div v-if="row.name"
class="c-form__row__label label flex-elem"
:title="row.description"
>
{{ row.name }}
</div>
<div class="c-form__row__controls controls flex-elem">
<div v-if="row.control"
class="c-form__controls-wrapper wrapper"
>
<component
:is="getComponent"
:key="row.key"
:ref="`form-control-${row.key}`"
:model="row"
:value="row.value"
:required="isRequired"
@onChange="onChange"
/>
</div>
</div>
</div>
</template>
<script>
import AutoCompleteField from "@/api/forms/components/controls/AutoCompleteField.vue";
import Composite from "@/api/forms/components/controls/Composite.vue";
import FileInput from "@/api/forms/components/controls/FileInput.vue"
import Locator from "@/api/forms/components/controls/Locator.vue";
import NumberField from "@/api/forms/components/controls/NumberField.vue";
import SelectField from '@/api/forms/components/controls/SelectField.vue';
import TextArea from "@/api/forms/components/controls/TextArea.vue";
import TextField from "@/api/forms/components/controls/TextField.vue";
const CONTROL_TYPE_VIEW_MAP = {
'autocomplete': AutoCompleteField,
'composite': Composite,
'file-input': FileInput,
'locator': Locator,
'numberfield': NumberField,
'select': SelectField,
'textarea': TextArea,
'textfield': TextField,
};
export default {
name: 'FormRow',
components: CONTROL_TYPE_VIEW_MAP,
inject: ['openmct', 'domainObject'],
props: {
cssClass: {
type: String,
default: '',
required: true
},
first: {
type: Boolean,
default: false,
required: true
},
row: {
type: Object,
required: true
}
},
data() {
return {
valid: undefined,
visited: false
};
},
computed: {
getComponent() {
return CONTROL_TYPE_VIEW_MAP[this.row.control];
},
isRequired() {
//TODO: Check if field is required
return false;
},
rowClass() {
let cssClass = this.cssClass;
if (this.first === true) {
cssClass = `${cssClass} first`;
}
if (this.row.required) {
cssClass = `${cssClass} req`;
}
if (this.row.layout === 'controls-first') {
cssClass = `${cssClass} l-controls-first`;
}
if (this.row.layout === 'controls-under') {
cssClass = `${cssClass} l-controls-under`;
}
if (this.visited && this.valid !== undefined) {
if (this.valid === true) {
cssClass = `${cssClass} valid`;
} else {
cssClass = `${cssClass} invalid`;
}
}
return cssClass;
}
},
mounted() {
if (this.row.required) {
const data = {
model: this.row,
value: this.row.value
};
this.onChange(data, false);
}
},
methods: {
onChange(data, visited = true) {
this.visited = visited;
const valid = this.validateRow(data);
this.valid = valid;
data.invalid = !valid;
this.$emit('onChange', data);
},
validateRow(data) {
let valid = true;
if (this.row.required) {
valid = data.value !== undefined && data.value !== null && data.value !== '';
}
const pattern = data.model.pattern;
if (valid && pattern) {
const regex = new RegExp(pattern);
valid = regex.test(data.value);
}
const validate = data.model.validate;
if (valid && validate) {
valid = validate(this.domainObject, data);
}
return valid;
}
}
};
</script>

View File

@@ -0,0 +1,175 @@
<!--
Open MCT, Copyright (c) 2014-2021, United States Government
as represented by the Administrator of the National Aeronautics and Space
Administration. All rights reserved.
Open MCT is licensed under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0.
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
License for the specific language governing permissions and limitations
under the License.
Open MCT includes source code licensed under additional open source
licenses. See the Open Source Licenses file (LICENSES.md) included with
this source code distribution or the Licensing information page available
at runtime from the About dialog for additional information.
-->
<template>
<div class="form-control autocomplete">
<input v-model="field"
class="autocompleteInput"
type="text"
@click="inputClicked()"
@keydown="keyDown($event)"
>
<span class="icon-arrow-down"
@click="arrowClicked()"
></span>
<div
class="autocompleteOptions"
@blur="hideOptions = true"
>
<ul v-if="!hideOptions">
<li v-for="opt in filteredOptions"
:key="opt.optionId"
:class="{'optionPreSelected': optionIndex === opt.optionId}"
@click="fillInputWithString(opt.name)"
@mouseover="optionMouseover(opt.optionId)"
>
<span class="optionText">{{ opt.name }}</span>
</li>
</ul>
</div>
</div>
</template>
<script>
const key = {
down: 40,
up: 38,
enter: 13
};
export default {
props: {
model: {
type: Object,
required: true
}
},
data() {
return {
hideOptions: true,
optionIndex: 0,
field: this.model.value
};
},
computed : {
filteredOptions() {
const options = this.optionNames || [];
return options
.filter(option => {
return option.toLowerCase().indexOf(this.field.toLowerCase()) >= 0;
}).map((option, index) => {
return {
optionId: index,
name: option
};
});
}
},
mounted() {
this.options = this.model.options;
this.autocompleteInputElement = this.$el.getElementsByClassName('autocompleteInput')[0];
if (this.options[0].name) {
// If "options" include name, value pair
this.optionNames = this.options.map((opt) => {
return opt.name;
});
} else {
// If options is only an array of string.
this.optionNames = this.options;
}
},
methods: {
decrementOptionIndex() {
if (this.optionIndex === 0) {
this.optionIndex = this.filteredOptions.length;
}
this.optionIndex--;
this.scrollIntoView();
},
incrementOptionIndex() {
if (this.optionIndex === this.filteredOptions.length - 1) {
this.optionIndex = -1;
}
this.optionIndex++;
this.scrollIntoView();
},
fillInputWithString(string) {
this.hideOptions = true;
this.field = string;
},
showOptions(string) {
this.hideOptions = false;
this.optionIndex = 0;
},
keyDown($event) {
if (this.filteredOptions) {
let keyCode = $event.keyCode;
switch (keyCode) {
case key.down:
this.incrementOptionIndex();
break;
case key.up:
$event.preventDefault(); // Prevents cursor jumping back and forth
this.decrementOptionIndex();
break;
case key.enter:
if (this.filteredOptions[this.optionIndex]) {
this.fillInputWithString(this.filteredOptions[this.optionIndex].name);
}
}
}
},
inputClicked() {
this.autocompleteInputElement.select();
this.showOptions(this.autocompleteInputElement.value);
},
arrowClicked() {
this.autocompleteInputElement.select();
this.showOptions('');
},
optionMouseover(optionId) {
this.optionIndex = optionId;
},
scrollIntoView() {
setTimeout(() => {
const element = this.$el.querySelector('.optionPreSelected');
element && element.scrollIntoView({behavior: 'smooth', block: 'center', inline: 'nearest'});
});
}
},
watch: {
field(newValue, oldValue) {
if (newValue !== oldValue) {
const data = {
model: this.model,
value: newValue
};
this.$emit('onChange', data);
}
}
}
};
</script>

View File

@@ -0,0 +1,56 @@
<!--
Open MCT, Copyright (c) 2014-2021, United States Government
as represented by the Administrator of the National Aeronautics and Space
Administration. All rights reserved.
Open MCT is licensed under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0.
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
License for the specific language governing permissions and limitations
under the License.
Open MCT includes source code licensed under additional open source
licenses. See the Open Source Licenses file (LICENSES.md) included with
this source code distribution or the Licensing information page available
at runtime from the About dialog for additional information.
-->
<template>
<span>
<CompositeItem v-for="(item, index) in model.items"
:key="item.name"
:first="index < 1"
:value="JSON.stringify(model.value[index])"
:item="item"
@onChange="onChange"
/>
</span>
</template>
<script>
import CompositeItem from "@/api/forms/components/controls/CompositeItem.vue";
export default {
components: {
CompositeItem
},
props: {
model: {
type: Object,
required: true
}
},
methods: {
onChange(data) {
this.$emit('onChange', data);
}
},
mounted() {
this.model.items.forEach((item, index) => item.key = `${this.model.key}.${index}`);
}
};
</script>

View File

@@ -0,0 +1,71 @@
<!--
Open MCT, Copyright (c) 2014-2021, United States Government
as represented by the Administrator of the National Aeronautics and Space
Administration. All rights reserved.
Open MCT is licensed under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0.
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
License for the specific language governing permissions and limitations
under the License.
Open MCT includes source code licensed under additional open source
licenses. See the Open Source Licenses file (LICENSES.md) included with
this source code distribution or the Licensing information page available
at runtime from the About dialog for additional information.
-->
<template>
<div :class="compositeCssClass">
<FormRow :css-class="item.cssClass"
:first="first"
:row="row"
@onChange="onChange"
/>
<span class="composite-control-label">
{{ item.name }}
</span>
</div>
</template>
<script>
export default {
components: {
FormRow: () => import('@/api/forms/components/FormRow.vue')
},
props: {
item: {
type: Object,
required: true
},
first: {
type: Boolean,
required: true
},
value: {
type: String
}
},
computed: {
compositeCssClass() {
return `l-composite-control l-${this.item.control}`;
},
row() {
const row = this.item;
row.value = JSON.parse(this.value);
return row;
}
},
methods: {
onChange(data) {
this.$emit('onChange', data);
}
}
};
</script>

View File

@@ -0,0 +1,71 @@
<template>
<span class="form-control shell">
<span class="field control"
:class="model.cssClass"
>
<input ref="fileInput" id="fileElem" type="file" accept=".json" style="display:none">
<button id="fileSelect"
class="c-button"
@click="selectFile"
>{{ name }}</button>
</span>
</span>
</template>
<script>
export default {
inject: ['openmct'],
props: {
model: {
type: Object,
required: true
}
},
data() {
return {
fileInfo: undefined
};
},
computed: {
name() {
const fileInfo = this.fileInfo || this.model.value;
return fileInfo && fileInfo.name || this.model.text;
}
},
mounted() {
this.$refs.fileInput.addEventListener("change", this.handleFiles, false);
},
methods: {
handleFiles() {
const fileList = this.$refs.fileInput.files;
this.readFile(fileList[0]);
},
readFile(file) {
const self = this;
const fileReader = new FileReader();
const fileInfo = {};
fileInfo.name = file.name;
fileReader.onload = function (event) {
fileInfo.body = event.target.result;
self.fileInfo = fileInfo;
const data = {
model: self.model,
value: fileInfo
};
self.$emit('onChange', data);
};
fileReader.onerror = function (error) {
console.error('fileReader error', error);
};
fileReader.readAsText(file);
},
selectFile() {
this.$refs.fileInput.click();
}
}
};
</script>

View File

@@ -0,0 +1,50 @@
<template>
<ConditionSetSelectorDialog
:hideTitle="true"
:ignoreTypeCheck="true"
:cssClass="`form-locator`"
:parent="model.parent"
@conditionSetSelected="handleItemSelection"
/>
</template>
<script>
import ConditionSetSelectorDialog from '@/plugins/condition/components/inspector/ConditionSetSelectorDialog.vue';
export default {
components: {
ConditionSetSelectorDialog
},
inject: ['openmct'],
props: {
model: {
type: Object,
required: true
}
},
data() {
return {
};
},
computed: {
},
watch: {
},
mounted() {
// remove following after css fix
setTimeout(() => {
document.querySelector('.c-overlay__contents-main').style.height = '200px';
});
},
destroyed() {
},
methods: {
handleItemSelection(parentDomainObject) {
const data = { model: this.model, value: parentDomainObject };
this.$emit('onChange', data);
}
}
};
</script>

View File

@@ -0,0 +1,62 @@
<!--
Open MCT, Copyright (c) 2014-2021, United States Government
as represented by the Administrator of the National Aeronautics and Space
Administration. All rights reserved.
Open MCT is licensed under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0.
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
License for the specific language governing permissions and limitations
under the License.
Open MCT includes source code licensed under additional open source
licenses. See the Open Source Licenses file (LICENSES.md) included with
this source code distribution or the Licensing information page available
at runtime from the About dialog for additional information.
-->
<template>
<span class="form-control shell">
<span class="field control"
:class="model.cssClass"
>
<input v-model="field"
type="number"
:min="model.min"
:max="model.max"
:step="model.step"
@blur="blur()"
>
</span>
</span>
</template>
<script>
export default {
props: {
model: {
type: Object,
required: true
}
},
data() {
return {
field: this.model.value
};
},
methods: {
blur() {
const data = {
model :this.model,
value: this.field
};
this.$emit('onChange', data);
}
}
};
</script>

View File

@@ -0,0 +1,42 @@
<template>
<div class='form-control SelectField'>
<select required="model.required"
name="mctControl"
@change="onChange($event)"
v-model="selected"
>
<option v-for="option in model.options"
:key="option.name"
:value="option.value"
>
{{ option.name }}
</option>
</select>
</div>
</template>
<script>
export default {
props: {
model: {
type: Object,
required: true
}
},
data() {
return {
selected: this.model.value
};
},
methods: {
onChange() {
const data = {
model: this.model,
value: this.selected
};
this.$emit('onChange', data);
}
}
};
</script>

View File

@@ -0,0 +1,60 @@
<!--
Open MCT, Copyright (c) 2014-2021, United States Government
as represented by the Administrator of the National Aeronautics and Space
Administration. All rights reserved.
Open MCT is licensed under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0.
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
License for the specific language governing permissions and limitations
under the License.
Open MCT includes source code licensed under additional open source
licenses. See the Open Source Licenses file (LICENSES.md) included with
this source code distribution or the Licensing information page available
at runtime from the About dialog for additional information.
-->
<template>
<span class="form-control shell">
<span class="field control"
:class="model.cssClass"
>
<textarea v-model="field"
type="text"
:size="model.size"
@blur="blur()"
/>
</span>
</span>
</template>
<script>
export default {
props: {
model: {
type: Object,
required: true
}
},
data() {
return {
field: this.model.value
};
},
methods: {
blur() {
const data = {
model :this.model,
value: this.field
};
this.$emit('onChange', data);
}
}
};
</script>

View File

@@ -1,5 +1,5 @@
<!--
Open MCT, Copyright (c) 2014-2020, United States Government
Open MCT, Copyright (c) 2014-2021, United States Government
as represented by the Administrator of the National Aeronautics and Space
Administration. All rights reserved.
@@ -19,33 +19,41 @@
this source code distribution or the Licensing information page available
at runtime from the About dialog for additional information.
-->
<template>
<li class="c-inspect-properties__row">
<div class="c-inspect-properties__label">
{{ label }}
</div>
<div class="c-inspect-properties__value">
{{ value }}
</div>
</li>
<span class="form-control shell">
<span class="field control"
:class="model.cssClass"
>
<input v-model="field"
type="text"
:size="model.size"
@blur="blur()"
/>
</span>
</span>
</template>
<script>
export default {
props: {
label: {
type: String,
default() {
return '';
}
},
value: {
type: String,
default() {
return '';
}
model: {
type: Object,
required: true
}
},
data() {
return {
field: this.model.value
};
},
methods: {
blur() {
const data = {
model :this.model,
value: this.field
};
this.$emit('onChange', data);
}
}
};

View File

@@ -45,8 +45,6 @@ function ObjectAPI(typeRegistry, openmct) {
this.rootProvider = new RootObjectProvider(this.rootRegistry);
this.cache = {};
this.interceptorRegistry = new InterceptorRegistry();
this.SYNCHRONIZED_OBJECT_TYPES = ['notebook', 'plan'];
}
/**
@@ -399,25 +397,20 @@ ObjectAPI.prototype._toMutable = function (object) {
mutableObject = object;
} else {
mutableObject = MutableDomainObject.createMutable(object, this.eventEmitter);
}
// Check if provider supports realtime updates
let identifier = utils.parseKeyString(mutableObject.identifier);
let provider = this.getProvider(identifier);
// Check if provider supports realtime updates
let identifier = utils.parseKeyString(mutableObject.identifier);
let provider = this.getProvider(identifier);
if (provider !== undefined
&& provider.observe !== undefined
&& this.SYNCHRONIZED_OBJECT_TYPES.includes(object.type)) {
let unobserve = provider.observe(identifier, (updatedModel) => {
if (updatedModel.persisted > mutableObject.modified) {
//Don't replace with a stale model. This can happen on slow connections when multiple mutations happen
//in rapid succession and intermediate persistence states are returned by the observe function.
mutableObject.$refresh(updatedModel);
}
});
mutableObject.$on('$_destroy', () => {
unobserve();
});
}
if (provider !== undefined
&& provider.observe !== undefined) {
let unobserve = provider.observe(identifier, (updatedModel) => {
mutableObject.$refresh(updatedModel);
});
mutableObject.$on('$destroy', () => {
unobserve();
});
}
return mutableObject;

View File

@@ -163,22 +163,14 @@ describe("The Object API", () => {
key: 'test-key'
},
name: 'test object',
type: 'notebook',
otherAttribute: 'other-attribute-value',
modified: 0,
persisted: 0,
objectAttribute: {
embeddedObject: {
embeddedKey: 'embedded-value'
}
}
};
updatedTestObject = Object.assign({
otherAttribute: 'changed-attribute-value'
}, testObject);
updatedTestObject.modified = 1;
updatedTestObject.persisted = 1;
updatedTestObject = Object.assign({otherAttribute: 'changed-attribute-value'}, testObject);
mockProvider = jasmine.createSpyObj("mock provider", [
"get",
"create",
@@ -190,8 +182,6 @@ describe("The Object API", () => {
mockProvider.observeObjectChanges.and.callFake(() => {
callbacks[0](updatedTestObject);
callbacks.splice(0, 1);
return () => {};
});
mockProvider.observe.and.callFake((id, callback) => {
if (callbacks.length === 0) {
@@ -199,8 +189,6 @@ describe("The Object API", () => {
} else {
callbacks[0] = callback;
}
return () => {};
});
objectAPI.addProvider(TEST_NAMESPACE, mockProvider);

View File

@@ -21,7 +21,8 @@
&__outer {
@include abs();
background: $colorBodyBg;
background: $overlayColorBg;
color: $overlayColorFg;
display: flex;
flex-direction: column;
padding: $overlayInnerMargin;
@@ -29,6 +30,7 @@
&__close-button {
$p: $interiorMargin + 2px;
color: $overlayColorFg;
font-size: 1.5em;
position: absolute;
top: $p; right: $p;
@@ -80,6 +82,11 @@
}
}
.c-button,
.c-click-icon {
filter: $overlayBrightnessAdjust;
}
.c-object-label__name {
filter: $objectLabelNameFilter;
}
@@ -96,7 +103,6 @@ body.desktop {
}
// Overlay types, styling for desktop. Appended to .l-overlay-wrapper element.
.l-overlay-large,
.l-overlay-small,
.l-overlay-fit {
.c-overlay__outer {
@@ -118,8 +124,12 @@ body.desktop {
$tbPad: floor($pad * 0.8);
$lrPad: $pad;
.c-overlay {
&__blocker {
display: none;
}
&__outer {
@include overlaySizing($overlayOuterMarginLarge);
@include overlaySizing($overlayOuterMarginFullscreen);
padding: $tbPad $lrPad;
}

View File

@@ -567,24 +567,21 @@ define([
* @method limits returns a limits object of
* type {
* level1: {
* low: { key1: value1, key2: value2, color: <supportedColor> },
* high: { key1: value1, key2: value2, color: <supportedColor> }
* low: { key1: value1, key2: value2 },
* high: { key1: value1, key2: value2 }
* },
* level2: {
* low: { key1: value1, key2: value2 },
* high: { key1: value1, key2: value2 }
* }
* }
* supported colors are purple, red, orange, yellow and cyan
* @memberof module:openmct.TelemetryAPI~TelemetryProvider#
*/
TelemetryAPI.prototype.getLimits = function (domainObject) {
const provider = this.findLimitEvaluator(domainObject);
if (!provider || !provider.getLimits) {
if (!provider) {
return {
limits: function () {
return Promise.resolve(undefined);
}
limits: function () {}
};
}

View File

@@ -40,7 +40,6 @@ const DEFAULTS = [
'platform/features/clock',
'platform/features/hyperlink',
'platform/features/timeline',
'platform/forms',
'platform/identity',
'platform/persistence/aggregator',
'platform/persistence/queue',
@@ -85,7 +84,6 @@ define([
'../platform/features/hyperlink/bundle',
'../platform/features/static-markup/bundle',
'../platform/features/timeline/bundle',
'../platform/forms/bundle',
'../platform/framework/bundle',
'../platform/framework/src/load/Bundle',
'../platform/identity/bundle',

View File

@@ -73,7 +73,7 @@ describe('the plugin', function () {
});
it('provides a folder to hold plans', () => {
return openmct.objects.get(identifier).then((object) => {
openmct.objects.get(identifier).then((object) => {
expect(object).toEqual({
identifier,
type: 'folder',
@@ -83,7 +83,7 @@ describe('the plugin', function () {
});
it('provides composition for couch search folders', () => {
return composition.load().then((objects) => {
composition.load().then((objects) => {
expect(objects.length).toEqual(2);
});
});

View File

@@ -30,7 +30,7 @@ export default function plugin() {
openmct.objectViews.addProvider(new LADTableSetViewProvider(openmct));
openmct.types.addType('LadTable', {
name: "Latest Data Table",
name: "LAD Table",
creatable: true,
description: "A Latest Available Data tabular view in which each row displays the values for one or more contained telemetry objects.",
cssClass: 'icon-tabular-lad',

View File

@@ -67,6 +67,10 @@ describe("The LAD Table", () => {
// this setups up the app
beforeEach((done) => {
const appHolder = document.createElement('div');
appHolder.style.width = '640px';
appHolder.style.height = '480px';
openmct = createOpenMct();
parent = document.createElement('div');
@@ -86,7 +90,7 @@ describe("The LAD Table", () => {
});
openmct.on('start', done);
openmct.startHeadless();
openmct.startHeadless(appHolder);
});
afterEach(() => {
@@ -109,8 +113,7 @@ describe("The LAD Table", () => {
beforeEach(() => {
ladTableCompositionCollection = openmct.composition.get(mockObj.ladTable);
return ladTableCompositionCollection.load();
ladTableCompositionCollection.load();
});
it("should accept telemetry producing objects", () => {
@@ -189,6 +192,8 @@ describe("The LAD Table", () => {
await Promise.all([telemetryRequestPromise, telemetryObjectPromise, anotherTelemetryObjectPromise]);
await Vue.nextTick();
return;
});
it("should show one row per object in the composition", () => {
@@ -237,6 +242,13 @@ describe("The LAD Table Set", () => {
let ladPlugin;
let parent;
let child;
let telemetryCount = 3;
let timeFormat = 'utc';
let mockTelemetry = getMockTelemetry({
count: telemetryCount,
format: timeFormat
});
let mockObj = getMockObjects({
objectKeyStrings: ['ladTable', 'ladTableSet', 'telemetry']
@@ -252,22 +264,31 @@ describe("The LAD Table Set", () => {
mockObj.ladTableSet.composition.push(mockObj.ladTable.identifier);
beforeEach((done) => {
const appHolder = document.createElement('div');
appHolder.style.width = '640px';
appHolder.style.height = '480px';
openmct = createOpenMct();
parent = document.createElement('div');
child = document.createElement('div');
parent.appendChild(child);
spyOn(openmct.telemetry, 'request').and.returnValue(Promise.resolve([]));
ladPlugin = new LadPlugin();
openmct.install(ladPlugin);
spyOn(openmct.objects, 'get').and.returnValue(Promise.resolve({}));
openmct.time.bounds({
start: bounds.start,
end: bounds.end
});
openmct.on('start', done);
openmct.startHeadless();
openmct.start(appHolder);
});
afterEach(() => {
@@ -280,8 +301,6 @@ describe("The LAD Table Set", () => {
});
it("should provide a lad table set view only for lad table set objects", () => {
spyOn(openmct.objects, 'get').and.returnValue(Promise.resolve({}));
let applicableViews = openmct.objectViews.get(mockObj.ladTableSet, []);
let ladTableSetView = applicableViews.find(
@@ -296,11 +315,8 @@ describe("The LAD Table Set", () => {
let ladTableSetCompositionCollection;
beforeEach(() => {
spyOn(openmct.objects, 'get').and.returnValue(Promise.resolve({}));
ladTableSetCompositionCollection = openmct.composition.get(mockObj.ladTableSet);
return ladTableSetCompositionCollection.load();
ladTableSetCompositionCollection.load();
});
it("should accept lad table objects", () => {
@@ -338,17 +354,41 @@ describe("The LAD Table Set", () => {
otherObj.ladTable.composition.push(mockObj.telemetry.identifier);
mockObj.ladTableSet.composition.push(otherObj.ladTable.identifier);
beforeEach(() => {
spyOn(openmct.telemetry, 'request').and.returnValue(Promise.resolve([]));
beforeEach(async () => {
let telemetryRequestResolve;
let ladObjectResolve;
let anotherLadObjectResolve;
spyOn(openmct.objects, 'get').and.callFake((obj) => {
let telemetryRequestPromise = new Promise((resolve) => {
telemetryRequestResolve = resolve;
});
let ladObjectPromise = new Promise((resolve) => {
ladObjectResolve = resolve;
});
let anotherLadObjectPromise = new Promise((resolve) => {
anotherLadObjectResolve = resolve;
});
openmct.telemetry.request.and.callFake(() => {
telemetryRequestResolve(mockTelemetry);
return telemetryRequestPromise;
});
openmct.objects.get.and.callFake((obj) => {
if (obj.key === 'lad-object') {
return Promise.resolve(mockObj.ladTable);
ladObjectResolve(mockObj.ladObject);
return ladObjectPromise;
} else if (obj.key === 'another-lad-object') {
return Promise.resolve(otherObj.ladTable);
} else if (obj.key === 'telemetry-object') {
return Promise.resolve(mockObj.telemetry);
anotherLadObjectResolve(otherObj.ladObject);
return anotherLadObjectPromise;
}
return Promise.resolve({});
});
openmct.time.bounds({
@@ -359,19 +399,20 @@ describe("The LAD Table Set", () => {
applicableViews = openmct.objectViews.get(mockObj.ladTableSet, []);
ladTableSetViewProvider = applicableViews.find((viewProvider) => viewProvider.key === ladTableSetKey);
ladTableSetView = ladTableSetViewProvider.view(mockObj.ladTableSet, [mockObj.ladTableSet]);
ladTableSetView.show(child);
ladTableSetView.show(child, true);
return Vue.nextTick();
await Promise.all([telemetryRequestPromise, ladObjectPromise, anotherLadObjectPromise]);
await Vue.nextTick();
return;
});
it("should show one row per lad table object in the composition", () => {
const ladTableSetCompositionCollection = openmct.composition.get(mockObj.ladTableSet);
const rowCount = parent.querySelectorAll(LAD_SET_TABLE_HEADERS).length;
return ladTableSetCompositionCollection.load().then(() => {
const rowCount = parent.querySelectorAll(LAD_SET_TABLE_HEADERS).length;
expect(rowCount).toBe(mockObj.ladTableSet.composition.length);
});
expect(rowCount).toBe(mockObj.ladTableSet.composition.length);
pending();
});
});
});

View File

@@ -22,14 +22,12 @@
define(
[
"utils/testing",
"./URLIndicator",
"./URLIndicatorPlugin",
"../../MCT",
"zepto"
],
function (
testingUtils,
URLIndicator,
URLIndicatorPlugin,
MCT,
@@ -46,7 +44,7 @@ define(
beforeEach(function () {
jasmine.clock().install();
openmct = new testingUtils.createOpenMct();
openmct = new MCT();
spyOn(openmct.indicators, 'add');
spyOn($, 'ajax');
$.ajax.and.callFake(function (options) {
@@ -57,8 +55,6 @@ define(
afterEach(function () {
$.ajax = defaultAjaxFunction;
jasmine.clock().uninstall();
return testingUtils.resetApplicationState(openmct);
});
describe("on initialization", function () {

View File

@@ -28,10 +28,8 @@ import {
resetApplicationState,
spyOnBuiltins
} from 'utils/testing';
import Vue from 'vue';
// TODO lots of its without expects
xdescribe("AutoflowTabularPlugin", () => {
describe("AutoflowTabularPlugin", () => {
let testType;
let testObject;
let mockmct;
@@ -53,7 +51,7 @@ xdescribe("AutoflowTabularPlugin", () => {
});
afterEach(() => {
return resetApplicationState(mockmct);
resetApplicationState(mockmct);
});
it("installs a view provider", () => {
@@ -103,7 +101,7 @@ xdescribe("AutoflowTabularPlugin", () => {
});
}
beforeEach(() => {
beforeEach((done) => {
callbacks = {};
spyOnBuiltins(['requestAnimationFrame']);
@@ -182,7 +180,7 @@ xdescribe("AutoflowTabularPlugin", () => {
view = provider.view(testObject);
view.show(testContainer);
return Vue.nextTick();
return done();
});
afterEach(() => {

View File

@@ -1,63 +0,0 @@
export default function install(openmct) {
openmct.objectViews.addProvider({
name: "Data Table",
key: "data-table",
cssClass: "icon-packet",
description: "Tabular view of telemetry",
canView(domainObject) {
return true;
},
view(domainObject) {
return {
async show(element) {
let telemetryMetadata = openmct.telemetry.getMetadata(domainObject).values();
let table = document.createElement('table');
let tableHead = document.createElement('thead');
let tableBody = document.createElement('tbody');
let tableHeadRow = document.createElement('tr');
tableHead.appendChild(tableHeadRow);
table.appendChild(tableHead);
table.appendChild(tableBody);
element.appendChild(table);
telemetryMetadata.forEach(metadatum => {
let tableHeadCell = document.createElement('td');
tableHeadRow.appendChild(tableHeadCell);
tableHeadCell.innerText = metadatum.name;
});
async function requestTelemetry() {
let telemetry = await openmct.telemetry.request(domainObject);
telemetry.forEach((datum) => {
let dataRow = document.createElement('tr');
telemetryMetadata.forEach(metadatum => {
let dataCell = document.createElement('td');
let formatter = openmct.telemetry.getValueFormatter(metadatum);
let telemetryValue = formatter.format(datum[metadatum.key]);
dataCell.innerText = telemetryValue;
dataRow.appendChild(dataCell);
});
tableBody.appendChild(dataRow);
});
}
openmct.time.on('bounds', () => {
tableBody.innerHTML = '';
requestTelemetry();
});
requestTelemetry();
// openmct.telemetry.subscribe(domainObject, (datum) => {
// element.innerText = JSON.stringify(datum);
// });
},
destroy() {
}
};
}
});
}

View File

@@ -27,17 +27,15 @@ export default class StyleRuleManager extends EventEmitter {
super();
this.openmct = openmct;
this.callback = callback;
this.refreshData = this.refreshData.bind(this);
this.toggleSubscription = this.toggleSubscription.bind(this);
if (suppressSubscriptionOnEdit) {
this.openmct.editor.on('isEditing', this.toggleSubscription);
this.openmct.editor.on('isEditing', this.toggleSubscription.bind(this));
this.isEditing = this.openmct.editor.editing;
}
if (styleConfiguration) {
this.initialize(styleConfiguration);
if (styleConfiguration.conditionSetIdentifier) {
this.openmct.time.on("bounds", this.refreshData);
this.openmct.time.on("bounds", this.refreshData.bind(this));
this.subscribeToConditionSet();
} else {
this.applyStaticStyle();

View File

@@ -58,6 +58,7 @@
:node="child"
:selected-item="selectedItem"
:handle-item-selected="handleItemSelected"
:navigateToParent="navigateToParent"
/>
</ul>
</li>
@@ -88,7 +89,13 @@ export default {
default() {
return (item) => {};
}
}
},
navigateToParent: {
type: String,
default() {
return undefined;
}
},
},
data() {
return {
@@ -147,6 +154,16 @@ export default {
},
mounted() {
this.domainObject = this.node.object;
if (this.navigateToParent && this.navigateToParent.includes(this.openmct.objects.makeKeyString(this.domainObject.identifier))) {
this.expanded = true;
}
if (this.navigateToParent && this.navigateToParent.endsWith(this.openmct.objects.makeKeyString(this.domainObject.identifier))) {
this.handleItemSelected(this.node.object, this.node)
this.$el.scrollIntoView();
}
let removeListener = this.openmct.objects.observe(this.domainObject, '*', (newObject) => {
this.domainObject = newObject;
});

View File

@@ -22,10 +22,14 @@
<template>
<div class="u-contents">
<div class="c-overlay__top-bar">
<div v-if="!hideTitle"
class="c-overlay__top-bar"
>
<div class="c-overlay__dialog-title">Select Condition Set</div>
</div>
<div class="c-overlay__contents-main c-selector c-tree-and-search">
<div class="c-overlay__contents-main c-selector c-tree-and-search"
:class="cssClass"
>
<div class="c-tree-and-search__search">
<search ref="shell-search"
class="c-search"
@@ -58,6 +62,7 @@
:node="treeItem"
:selected-item="selectedItem"
:handle-item-selected="handleItemSelection"
:navigateToParent="navigateToParent"
/>
</ul>
<!-- end main tree -->
@@ -91,6 +96,36 @@ export default {
ConditionSetDialogTreeItem
},
inject: ['openmct'],
props: {
cssClass: {
type: String,
required: false,
default() {
return '';
}
},
hideTitle: {
type: Boolean,
required: false,
default() {
return false;
}
},
ignoreTypeCheck: {
type: Boolean,
required: false,
default() {
return false;
}
},
parent: {
type: Object,
required: false,
default() {
return undefined;
}
}
},
data() {
return {
expanded: false,
@@ -98,7 +133,8 @@ export default {
allTreeItems: [],
filteredTreeItems: [],
isLoading: false,
selectedItem: undefined
selectedItem: undefined,
navigateToParent: undefined
};
},
computed: {
@@ -115,10 +151,24 @@ export default {
this.getDebouncedFilteredChildren = debounce(this.getFilteredChildren, 400);
},
mounted() {
this.getAllChildren();
if (this.parent) {
(async () => {
const objectPath = await this.openmct.objects.getOriginalPath(this.parent.identifier);
this.navigateToParent = '/browse/'
+ objectPath
.map(parent => this.openmct.objects.makeKeyString(parent.identifier))
.reverse()
.join('/');
this.getAllChildren(this.navigateToParent);
})();
} else {
this.getAllChildren();
}
},
methods: {
getAllChildren() {
getAllChildren(navigateToParent) {
this.isLoading = true;
this.openmct.objects.get('ROOT')
.then(root => {
@@ -131,7 +181,7 @@ export default {
id: this.openmct.objects.makeKeyString(c.identifier),
object: c,
objectPath: [c],
navigateToParent: '/browse'
navigateToParent: navigateToParent || '/browse'
};
});
});
@@ -178,7 +228,7 @@ export default {
}
},
handleItemSelection(item, node) {
if (item && item.type === 'conditionSet') {
if (item && (this.ignoreTypeCheck || item.type === 'conditionSet')) {
const parentId = (node.objectPath && node.objectPath.length > 1) ? node.objectPath[1].identifier : undefined;
this.selectedItem = {
itemId: item.identifier,

View File

@@ -21,10 +21,6 @@
margin-left: $interiorMargin;
}
&__value {
@include isLimit();
}
.c-frame & {
@include abs();
border: 1px solid transparent;

View File

@@ -34,54 +34,10 @@ export default class DuplicateAction {
}
async invoke(objectPath) {
let duplicationTask = new DuplicateTask(this.openmct);
let originalObject = objectPath[0];
let parent = objectPath[1];
let userInput;
let object = objectPath[0];
this.parent = objectPath[1];
try {
userInput = await this.getUserInput(originalObject, parent);
} catch (error) {
// user most likely canceled
return;
}
let newParent = userInput.location;
let inNavigationPath = this.inNavigationPath(originalObject);
// legacy check
if (this.isLegacyDomainObject(newParent)) {
newParent = await this.convertFromLegacy(newParent);
}
// if editing, save
if (inNavigationPath && this.openmct.editor.isEditing()) {
this.openmct.editor.save();
}
// duplicate
let newObject = await duplicationTask.duplicate(originalObject, newParent);
this.updateNameCheck(newObject, userInput.name);
return;
}
async getUserInput(originalObject, parent) {
let dialogService = this.openmct.$injector.get('dialogService');
let dialogForm = this.getDialogForm(originalObject, parent);
let formState = {
name: originalObject.name
};
let userInput = await dialogService.getUserInput(dialogForm, formState);
return userInput;
}
updateNameCheck(object, name) {
if (object.name !== name) {
object.name = name;
this.openmct.objects.save(object);
}
this.showForm(object, this.parent);
}
inNavigationPath(object) {
@@ -89,40 +45,66 @@ export default class DuplicateAction {
.some(objectInPath => this.openmct.objects.areIdsEqual(objectInPath.identifier, object.identifier));
}
getDialogForm(object, parent) {
return {
name: "Duplicate Item",
async onSave(object, changes, parent) {
console.log('onSave');
let inNavigationPath = this.inNavigationPath(object);
if (inNavigationPath && this.openmct.editor.isEditing()) {
this.openmct.editor.save();
}
if (changes.name && (changes.name !== object.name)) {
object.name = changes.name;
}
// duplicate
let duplicationTask = new DuplicateTask(this.openmct);
duplicationTask.duplicate(object, parent);
}
showForm(domainObject, parentDomainObject) {
const formStructure = {
title: "Duplicate Item",
sections: [
{
rows: [
{
key: "name",
control: "textfield",
name: "Name",
name: "Title",
pattern: "\\S+",
required: true,
cssClass: "l-input-lg"
cssClass: "l-input-lg",
value: domainObject.name
},
{
name: "Location",
cssClass: "grows",
name: "location",
control: "locator",
validate: this.validate(object, parent),
required: true,
parent: parentDomainObject,
validate: this.validate(parentDomainObject),
key: 'location'
}
]
}
]
};
this.openmct.forms.showForm(formStructure, {
domainObject,
parentDomainObject,
onSave: this.onSave.bind(this)
});
}
validate(object, currentParent) {
return (parentCandidate) => {
validate(currentParent) {
return (object, data) => {
const parentCandidate = data.value;
let currentParentKeystring = this.openmct.objects.makeKeyString(currentParent.identifier);
let parentCandidateKeystring = this.openmct.objects.makeKeyString(parentCandidate.getId());
let parentCandidateKeystring = this.openmct.objects.makeKeyString(parentCandidate.identifier);
let objectKeystring = this.openmct.objects.makeKeyString(object.identifier);
if (!parentCandidate || !currentParentKeystring) {
if (!parentCandidateKeystring || !currentParentKeystring) {
return false;
}
@@ -130,24 +112,15 @@ export default class DuplicateAction {
return false;
}
return this.openmct.composition.checkPolicy(
parentCandidate.useCapability('adapter'),
object
);
const parentCandidateComposition = parentCandidate.composition;
if (parentCandidateComposition && parentCandidateComposition.indexOf(objectKeystring) !== -1) {
return false;
}
return parentCandidate && this.openmct.composition.checkPolicy(parentCandidate, object);
};
}
isLegacyDomainObject(domainObject) {
return domainObject.getCapability !== undefined;
}
async convertFromLegacy(legacyDomainObject) {
let objectContext = legacyDomainObject.getCapability('context');
let domainObject = await this.openmct.objects.get(objectContext.domainObject.id);
return domainObject;
}
appliesTo(objectPath) {
let parent = objectPath[1];
let parentType = parent && this.openmct.types.get(parent.type);

View File

@@ -34,7 +34,6 @@ import uuid from 'uuid';
* @constructor
*/
export default class DuplicateTask {
constructor(openmct) {
this.domainObject = undefined;
this.parent = undefined;

View File

@@ -121,9 +121,10 @@ describe("The Duplicate Action plugin", () => {
describe("when moving an object to a new parent", () => {
beforeEach(async () => {
beforeEach(async (done) => {
duplicateTask = new DuplicateTask(openmct);
await duplicateTask.duplicate(parentObject, anotherParentObject);
done();
});
it("the duplicate child object's name (when not changing) should be the same as the original object", async () => {
@@ -142,15 +143,15 @@ describe("The Duplicate Action plugin", () => {
});
describe("when a new name is provided for the duplicated object", () => {
it("the name is updated", () => {
const NEW_NAME = 'New Name';
let childName;
const NEW_NAME = 'New Name';
beforeEach(() => {
duplicateTask = new DuplicateAction(openmct);
duplicateTask.updateNameCheck(parentObject, NEW_NAME);
});
childName = parentObject.name;
it("the name is updated", () => {
let childName = parentObject.name;
expect(childName).toEqual(NEW_NAME);
});
});

View File

@@ -23,11 +23,6 @@
body.mobile & {
flex: 1 0 auto;
}
[class*='l-overlay'] & {
// When this view is in an overlay, prevent navigation
pointer-events: none;
}
}
/******************************* GRID ITEMS */

View File

@@ -22,9 +22,4 @@
@include isAlias();
}
}
[class*='l-overlay'] & {
// When this view is in an overlay, prevent navigation
pointer-events: none;
}
}

View File

@@ -24,15 +24,10 @@ import {
resetApplicationState
} from 'utils/testing';
describe("the goToOriginalAction plugin", () => {
describe("the plugin", () => {
let openmct;
let goToOriginalAction;
let mockRootFolder;
let mockSubFolder;
let mockSubSubFolder;
let mockObject;
let goToFolderAction;
let mockObjectPath;
let hash;
beforeEach((done) => {
openmct = createOpenMct();
@@ -40,7 +35,7 @@ describe("the goToOriginalAction plugin", () => {
openmct.on('start', done);
openmct.startHeadless();
goToOriginalAction = openmct.actions._allActions.goToOriginal;
goToFolderAction = openmct.actions._allActions.goToOriginal;
});
afterEach(() => {
@@ -48,153 +43,34 @@ describe("the goToOriginalAction plugin", () => {
});
it('installs the go to folder action', () => {
expect(goToOriginalAction).toBeDefined();
expect(goToFolderAction).toBeDefined();
});
describe('when invoked', () => {
beforeEach(() => {
mockRootFolder = getMockObject('mock-root');
mockSubFolder = getMockObject('mock-sub');
mockSubSubFolder = getMockObject('mock-sub-sub');
mockObject = getMockObject('mock-table');
mockObjectPath = [{
name: 'mock folder',
type: 'folder',
identifier: {
key: 'mock-folder',
namespace: ''
}
}];
spyOn(openmct.objects, 'get').and.returnValue(Promise.resolve({
identifier: {
namespace: '',
key: 'test'
}
}));
mockObjectPath = [
mockObject,
mockSubSubFolder,
mockSubFolder,
mockRootFolder
];
spyOn(openmct.objects, 'get').and.callFake(identifier => {
const mockedObject = getMockObject(identifier);
return Promise.resolve(mockedObject);
});
spyOn(openmct.router, 'navigate').and.callFake(navigateTo => {
hash = navigateTo;
});
return goToOriginalAction.invoke(mockObjectPath);
goToFolderAction.invoke(mockObjectPath);
});
it('goes to the original location', () => {
const originalLocationHash = '#/browse/mock-root/mock-table';
return waitForNavigation(() => {
return hash === originalLocationHash;
}).then(() => {
expect(hash).toEqual(originalLocationHash);
});
it('goes to the original location', (done) => {
setTimeout(() => {
expect(window.location.href).toContain('context.html#/browse/?tc.mode=fixed&tc.startBound=0&tc.endBound=1&tc.timeSystem=utc');
done();
}, 1500);
});
});
function waitForNavigation(navigated) {
return new Promise((resolve, reject) => {
const start = Date.now();
checkNavigated();
function checkNavigated() {
const elapsed = Date.now() - start;
if (navigated()) {
resolve();
} else if (elapsed >= jasmine.DEFAULT_TIMEOUT_INTERVAL - 1000) {
reject("didn't navigate in time");
} else {
setTimeout(checkNavigated);
}
}
});
}
function getMockObject(key) {
const id = typeof key === 'string' ? key : key.key;
const mockMCTObjects = {
"ROOT": {
"composition": [
{
"namespace": "",
"key": "mock-root"
}
],
"identifier": {
"namespace": "",
"key": "mock-root"
}
},
"mock-root": {
"composition": [
{
"namespace": "",
"key": "mock-sub"
},
{
"namespace": "",
"key": "mock-table"
}
],
"name": "root",
"type": "folder",
"id": "mock-root",
"location": "ROOT",
"identifier": {
"namespace": "",
"key": "mock-root"
}
},
"mock-sub": {
"composition": [
{
"namespace": "",
"key": "mock-sub-sub"
},
{
"namespace": "",
"key": "mock-table"
}
],
"name": "sub",
"type": "folder",
"location": "mock-root",
"identifier": {
"namespace": "",
"key": "mock-sub"
}
},
"mock-table": {
"composition": [],
"configuration": {
"columnWidths": {},
"hiddenColumns": {}
},
"name": "table",
"type": "table",
"location": "mock-root",
"identifier": {
"namespace": "",
"key": "mock-table"
}
},
"mock-sub-sub": {
"composition": [
{
"namespace": "",
"key": "mock-table"
}
],
"name": "sub sub",
"type": "folder",
"location": "mock-sub",
"identifier": {
"namespace": "",
"key": "mock-sub-sub"
}
}
};
return mockMCTObjects[id];
}
});

View File

@@ -62,9 +62,6 @@ export default function ImageryViewProvider(openmct) {
destroy: function () {
component.$destroy();
component = undefined;
},
_getInstance: function () {
return component;
}
};
}

View File

@@ -124,40 +124,27 @@
</div>
</div>
<div
ref="thumbsWrapper"
class="c-imagery__thumbs-wrapper"
:class="[
{ 'is-paused': isPaused },
{ 'is-autoscroll-off': !resizingWindow && !autoScroll && !isPaused }
]"
:class="{'is-paused': isPaused}"
@scroll="handleScroll"
>
<div
ref="thumbsWrapper"
class="c-imagery__thumbs-scroll-area"
@scroll="handleScroll"
<div v-for="(image, index) in imageHistory"
:key="image.url + image.time"
class="c-imagery__thumb c-thumb"
:class="{ selected: focusedImageIndex === index && isPaused }"
@click="setFocusedImage(index, thumbnailClick)"
>
<div v-for="(image, index) in imageHistory"
:key="image.url + image.time"
class="c-imagery__thumb c-thumb"
:class="{ selected: focusedImageIndex === index && isPaused }"
@click="setFocusedImage(index, thumbnailClick)"
<a href=""
:download="image.imageDownloadName"
@click.prevent
>
<a href=""
:download="image.imageDownloadName"
@click.prevent
<img class="c-thumb__image"
:src="image.url"
>
<img class="c-thumb__image"
:src="image.url"
>
</a>
<div class="c-thumb__timestamp">{{ image.formattedTime }}</div>
</div>
</a>
<div class="c-thumb__timestamp">{{ image.formattedTime }}</div>
</div>
<button
class="c-imagery__auto-scroll-resume-button c-icon-button icon-play"
title="Resume automatic scrolling of image thumbnails"
@click="scrollToRight('reset')"
></button>
</div>
</div>
</template>
@@ -184,8 +171,6 @@ const TWENTYFOUR_HOURS = EIGHT_HOURS * 3;
const ARROW_RIGHT = 39;
const ARROW_LEFT = 37;
const SCROLL_LATENCY = 250;
export default {
components: {
Compass
@@ -219,8 +204,7 @@ export default {
focusedImageNaturalAspectRatio: undefined,
imageContainerWidth: undefined,
imageContainerHeight: undefined,
lockCompass: true,
resizingWindow: false
lockCompass: true
};
},
computed: {
@@ -396,13 +380,9 @@ export default {
this.imageContainerResizeObserver = new ResizeObserver(this.resizeImageContainer);
this.imageContainerResizeObserver.observe(this.$refs.focusedImage);
// For adjusting scroll bar size and position when resizing thumbs wrapper
this.handleScroll = _.debounce(this.handleScroll, SCROLL_LATENCY);
this.handleThumbWindowResizeEnded = _.debounce(this.handleThumbWindowResizeEnded, SCROLL_LATENCY);
this.thumbWrapperResizeObserver = new ResizeObserver(this.handleThumbWindowResizeStart);
this.thumbWrapperResizeObserver.observe(this.$refs.thumbsWrapper);
},
updated() {
this.scrollToRight();
},
beforeDestroy() {
if (this.unsubscribe) {
@@ -414,10 +394,6 @@ export default {
this.imageContainerResizeObserver.disconnect();
}
if (this.thumbWrapperResizeObserver) {
this.thumbWrapperResizeObserver.disconnect();
}
if (this.relatedTelemetry.hasRelatedTelemetry) {
this.relatedTelemetry.destroy();
}
@@ -585,15 +561,17 @@ export default {
},
handleScroll() {
const thumbsWrapper = this.$refs.thumbsWrapper;
if (!thumbsWrapper || this.resizingWindow) {
if (!thumbsWrapper) {
return;
}
const { scrollLeft, scrollWidth, clientWidth } = thumbsWrapper;
const disableScroll = scrollWidth > Math.ceil(scrollLeft + clientWidth);
const { scrollLeft, scrollWidth, clientWidth, scrollTop, scrollHeight, clientHeight } = thumbsWrapper;
const disableScroll = (scrollWidth - scrollLeft) > 2 * clientWidth
|| (scrollHeight - scrollTop) > 2 * clientHeight;
this.autoScroll = !disableScroll;
},
paused(state, type) {
this.isPaused = state;
if (type === 'button') {
@@ -606,7 +584,6 @@ export default {
}
this.autoScroll = true;
this.scrollToRight();
},
scrollToFocused() {
const thumbsWrapper = this.$refs.thumbsWrapper;
@@ -623,8 +600,8 @@ export default {
});
}
},
scrollToRight(type) {
if (type !== 'reset' && (this.isPaused || !this.$refs.thumbsWrapper || !this.autoScroll)) {
scrollToRight() {
if (this.isPaused || !this.$refs.thumbsWrapper || !this.autoScroll) {
return;
}
@@ -633,9 +610,7 @@ export default {
return;
}
this.$nextTick(() => {
this.$refs.thumbsWrapper.scrollLeft = scrollWidth;
});
setTimeout(() => this.$refs.thumbsWrapper.scrollLeft = scrollWidth, 0);
},
setFocusedImage(index, thumbnailClick = false) {
if (this.isPaused && !thumbnailClick) {
@@ -703,9 +678,9 @@ export default {
image.imageDownloadName = this.getImageDownloadName(datum);
this.imageHistory.push(image);
if (setFocused) {
this.setFocusedImage(this.imageHistory.length - 1);
this.scrollToRight();
}
},
getFormatter(key) {
@@ -841,24 +816,6 @@ export default {
this.imageContainerHeight = this.$refs.focusedImage.clientHeight;
}
},
handleThumbWindowResizeStart() {
if (!this.autoScroll) {
return;
}
// To hide resume button while scrolling
this.resizingWindow = true;
this.handleThumbWindowResizeEnded();
},
handleThumbWindowResizeEnded() {
if (!this.isPaused) {
this.scrollToRight('reset');
}
this.$nextTick(() => {
this.resizingWindow = false;
});
},
toggleLockCompass() {
this.lockCompass = !this.lockCompass;
}

View File

@@ -93,43 +93,24 @@
}
&__thumbs-wrapper {
display: flex; // Uses row layout
&.is-autoscroll-off {
background: $colorInteriorBorder;
[class*='__auto-scroll-resume-button'] {
display: block;
}
}
&.is-paused {
background: rgba($colorPausedBg, 0.4);
}
}
&__thumbs-scroll-area {
flex: 0 1 auto;
flex: 0 0 auto;
display: flex;
flex-direction: row;
height: 135px;
overflow-x: auto;
overflow-y: hidden;
margin-bottom: 1px;
padding-bottom: $interiorMarginSm;
&.is-paused {
background: rgba($colorPausedBg, 0.4);
}
.c-thumb:last-child {
// Hilite the lastest thumb
background: $colorBodyFg;
color: $colorBodyBg;
}
}
&__auto-scroll-resume-button {
display: none; // Set to block when __thumbs-wrapper has .is-autoscroll-off
flex: 0 0 auto;
font-size: 0.8em;
margin: $interiorMarginSm;
}
}
/*************************************** THUMBS */
@@ -161,7 +142,7 @@
.l-layout,
.c-fl {
.c-imagery__thumbs-scroll-area {
.c-imagery__thumbs-wrapper {
// When Imagery is in a layout, hide the thumbs area
display: none;
}

View File

@@ -92,7 +92,6 @@ describe("The Imagery View Layout", () => {
let resolveFunction;
let openmct;
let appHolder;
let parent;
let child;
let imageTelemetry = generateTelemetry(START - TEN_MINUTES, COUNT);
@@ -196,7 +195,7 @@ describe("The Imagery View Layout", () => {
// this setups up the app
beforeEach((done) => {
appHolder = document.createElement('div');
const appHolder = document.createElement('div');
appHolder.style.width = '640px';
appHolder.style.height = '480px';
@@ -210,8 +209,6 @@ describe("The Imagery View Layout", () => {
child = document.createElement('div');
parent.appendChild(child);
// document.querySelector('body').append(parent);
spyOn(window, 'ResizeObserver').and.returnValue({
observe() {},
disconnect() {}
@@ -365,21 +362,5 @@ describe("The Imagery View Layout", () => {
done();
});
});
it ('shows an auto scroll button when scroll to left', async () => {
// to mock what a scroll would do
imageryView._getInstance().$refs.ImageryLayout.autoScroll = false;
await Vue.nextTick();
let autoScrollButton = parent.querySelector('.c-imagery__auto-scroll-resume-button');
expect(autoScrollButton).toBeTruthy();
});
it ('scrollToRight is called when clicking on auto scroll button', async () => {
// use spyon to spy the scroll function
spyOn(imageryView._getInstance().$refs.ImageryLayout, 'scrollToRight');
imageryView._getInstance().$refs.ImageryLayout.autoScroll = false;
await Vue.nextTick();
parent.querySelector('.c-imagery__auto-scroll-resume-button').click();
expect(imageryView._getInstance().$refs.ImageryLayout.scrollToRight).toHaveBeenCalledWith('reset');
});
});
});

View File

@@ -30,6 +30,10 @@ describe('the plugin', function () {
const TEST_NAMESPACE = 'test';
beforeEach((done) => {
const appHolder = document.createElement('div');
appHolder.style.width = '640px';
appHolder.style.height = '480px';
openmct = createOpenMct();
openmct.install(new InterceptorPlugin(openmct));
@@ -42,7 +46,7 @@ describe('the plugin', function () {
element.appendChild(child);
openmct.on('start', done);
openmct.startHeadless();
openmct.startHeadless(appHolder);
});
afterEach(() => {
@@ -51,7 +55,6 @@ describe('the plugin', function () {
describe('the missingObjectInterceptor', () => {
let mockProvider;
beforeEach(() => {
mockProvider = jasmine.createSpyObj("mock provider", [
"get"
@@ -60,28 +63,27 @@ describe('the plugin', function () {
openmct.objects.addProvider(TEST_NAMESPACE, mockProvider);
});
it('returns missing objects', () => {
it('returns missing objects', (done) => {
const identifier = {
namespace: TEST_NAMESPACE,
key: 'hello'
};
return openmct.objects.get(identifier).then((testObject) => {
openmct.objects.get(identifier).then((testObject) => {
expect(testObject).toEqual({
identifier,
type: 'unknown',
name: 'Missing: test:hello'
});
done();
});
});
it('returns the My items object if not found', () => {
it('returns the My items object if not found', (done) => {
const identifier = {
namespace: TEST_NAMESPACE,
key: 'mine'
};
return openmct.objects.get(identifier).then((testObject) => {
openmct.objects.get(identifier).then((testObject) => {
expect(testObject).toEqual({
identifier,
"name": "My Items",
@@ -89,6 +91,7 @@ describe('the plugin', function () {
"composition": [],
"location": "ROOT"
});
done();
});
});

View File

@@ -0,0 +1,208 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, 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 LinkAction {
constructor(openmct) {
this.name = 'Create Link';
this.key = 'link';
this.description = 'Create Link to object in another location.';
this.cssClass = "icon-link";
this.group = "action";
this.priority = 7;
this.openmct = openmct;
}
invoke(objectPath) {
this.showForm(objectPath[0], objectPath[1]);
}
inNavigationPath(object) {
return this.openmct.router.path
.some(objectInPath => this.openmct.objects.areIdsEqual(objectInPath.identifier, object.identifier));
}
onSave(object, changes, parent) {
let inNavigationPath = this.inNavigationPath(object);
if (inNavigationPath && this.openmct.editor.isEditing()) {
this.openmct.editor.save();
}
this.linkInNewParent(object, parent);
}
linkInNewParent(child, newParent) {
let compositionCollection = this.openmct.composition.get(newParent);
compositionCollection.add(child);
}
showForm(domainObject, parentDomainObject) {
const formStructure = {
title: `Link "${domainObject.name}" to a New Location`,
sections: [
{
rows: [
{
name: "location",
control: "locator",
required: true,
validate: this.validate(parentDomainObject),
key: 'location'
}
]
}
]
};
this.openmct.forms.showForm(formStructure, {
domainObject,
parentDomainObject,
onSave: this.onSave.bind(this)
});
}
validate(currentParent) {
return (object, data) => {
const parentCandidate = data.value;
// console.log('move action : validateLocation', );
// TODO: remove getModel, checkPolicy and useCapability
let currentParentKeystring = this.openmct.objects.makeKeyString(currentParent.identifier);
let parentCandidateKeystring = this.openmct.objects.makeKeyString(parentCandidate.identifier);
let objectKeystring = this.openmct.objects.makeKeyString(object.identifier);
if (!parentCandidateKeystring || !currentParentKeystring) {
return false;
}
if (parentCandidateKeystring === currentParentKeystring) {
return false;
}
if (parentCandidateKeystring === objectKeystring) {
return false;
}
const parentCandidateComposition = parentCandidate.composition;
if (parentCandidateComposition && parentCandidateComposition.indexOf(objectKeystring) !== -1) {
return false;
}
return parentCandidate && this.openmct.composition.checkPolicy(parentCandidate, object);
};
}
appliesTo(objectPath) {
let domainObject = objectPath[0];
let type = domainObject && this.openmct.types.get(domainObject.type);
return type && type.definition.creatable;
}
// appliesTo(objectPath) {
// let parent = objectPath[1];
// let parentType = parent && this.openmct.types.get(parent.type);
// let child = objectPath[0];
// let childType = child && this.openmct.types.get(child.type);
// if (child.locked || (parent && parent.locked)) {
// return false;
// }
// return parentType
// && parentType.definition.creatable
// && childType
// && childType.definition.creatable
// && Array.isArray(parent.composition);
// }
// async invoke(objectPath) {
// let objectToLink = objectPath[0];
// let dialogService = this.openmct.$injector.get('dialogService');
// let dialogForm = this.getDialogForm(objectToLink);
// let userInput = await dialogService.getUserInput(dialogForm, {});
// let newParent = userInput.location;
// // legacy check
// if (this.isLegacyDomainObject(newParent)) {
// newParent = await this.convertFromLegacy(newParent);
// }
// this.linkInNewParent(objectToLink, newParent);
// }
// isLegacyDomainObject(domainObject) {
// return domainObject.getCapability !== undefined;
// }
// async convertFromLegacy(legacyDomainObject) {
// let objectContext = legacyDomainObject.getCapability('context');
// let domainObject = await this.openmct.objects.get(objectContext.domainObject.id);
// return domainObject;
// }
// getDialogForm(objectToLink) {
// let validate = this.validate(objectToLink);
// return {
// name: `Link "${objectToLink.name}" to a New Location`,
// sections: [
// {
// rows: [
// {
// name: "Link To",
// control: "locator",
// validate,
// key: 'location'
// }
// ]
// }
// ]
// };
// }
// validate(objectToLink) {
// return (parentObject) => {
// let parentCandidateKeystring = this.openmct.objects.makeKeyString(parentObject.getId());
// let objectToLinkKeystring = this.openmct.objects.makeKeyString(objectToLink.identifier);
// let sameObjectOrChildAlready = parentCandidateKeystring === objectToLinkKeystring
// || parentObject.getModel().composition.includes(objectToLinkKeystring);
// // the same object or a child already, not valid
// if (sameObjectOrChildAlready) {
// return false;
// }
// if (parentObject.getModel().locked) {
// return false;
// }
// // can contain
// return this.openmct.composition.checkPolicy(
// parentObject.useCapability('adapter'),
// objectToLink
// );
// };
// }
}

View File

@@ -19,10 +19,10 @@
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import OpenInNewTabAction from './openInNewTabAction';
import LinkAction from "./LinkAction";
export default function () {
return function (openmct) {
openmct.actions.register(new OpenInNewTabAction(openmct));
openmct.actions.register(new LinkAction(openmct));
};
}

View File

@@ -0,0 +1,124 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, 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 LinkActionPlugin from './plugin.js';
import LinkAction from './LinkAction.js';
import {
createOpenMct,
resetApplicationState,
getMockObjects
} from 'utils/testing';
describe("The Link Action plugin", () => {
let openmct;
let linkAction;
let childObject;
let parentObject;
let anotherParentObject;
const ORIGINAL_PARENT_ID = 'original-parent-object';
const LINK_ACITON_KEY = 'link';
beforeEach((done) => {
const appHolder = document.createElement('div');
appHolder.style.width = '640px';
appHolder.style.height = '480px';
openmct = createOpenMct();
childObject = getMockObjects({
objectKeyStrings: ['folder'],
overwrite: {
folder: {
name: "Child Folder",
location: ORIGINAL_PARENT_ID,
identifier: {
namespace: "",
key: "child-folder-object"
}
}
}
}).folder;
parentObject = getMockObjects({
objectKeyStrings: ['folder'],
overwrite: {
folder: {
name: "Parent Folder",
identifier: {
namespace: "",
key: "original-parent-object"
},
composition: [childObject.identifier]
}
}
}).folder;
anotherParentObject = getMockObjects({
objectKeyStrings: ['folder'],
overwrite: {
folder: {
name: "Another Parent Folder"
}
}
}).folder;
openmct.router.path = [childObject]; // preview action uses this in it's applyTo method
openmct.install(LinkActionPlugin());
openmct.on('start', done);
openmct.startHeadless(appHolder);
});
afterEach(() => {
resetApplicationState(openmct);
});
it("should be defined", () => {
expect(LinkActionPlugin).toBeDefined();
});
it("should make the link action available for an appropriate domainObject", () => {
let actions = openmct.actions.get([childObject]);
let action = actions.filter(a => a.key === LINK_ACITON_KEY);
expect(action.length).toEqual(1);
});
describe("when linking an object in a new parent", () => {
beforeEach(() => {
linkAction = new LinkAction(openmct);
linkAction.linkInNewParent(childObject, anotherParentObject);
});
it("the child object's identifier should be in the new parent's composition and location set to original parent", () => {
let newParentChild = anotherParentObject.composition[0];
expect(newParentChild).toEqual(childObject.identifier);
expect(childObject.location).toEqual(ORIGINAL_PARENT_ID)
});
it("the child object's identifier should remain in the original parent's composition", () => {
let oldParentCompositionChild = parentObject.composition[0];
expect(oldParentCompositionChild).toEqual(childObject.identifier);
});
});
});

View File

@@ -33,46 +33,9 @@ export default class MoveAction {
async invoke(objectPath) {
let object = objectPath[0];
let inNavigationPath = this.inNavigationPath(object);
let oldParent = objectPath[1];
let dialogService = this.openmct.$injector.get('dialogService');
let dialogForm = this.getDialogForm(object, oldParent);
let userInput;
this.oldParent = objectPath[1];
try {
userInput = await dialogService.getUserInput(dialogForm, { name: object.name });
} catch (err) {
// user canceled, most likely
return;
}
// if we need to update name
if (object.name !== userInput.name) {
this.openmct.objects.mutate(object, 'name', userInput.name);
}
let parentContext = userInput.location.getCapability('context');
let newParent = await this.openmct.objects.get(parentContext.domainObject.id);
if (inNavigationPath && this.openmct.editor.isEditing()) {
this.openmct.editor.save();
}
this.addToNewParent(object, newParent);
this.removeFromOldParent(oldParent, object);
if (inNavigationPath) {
let newObjectPath = await this.openmct.objects.getOriginalPath(object.identifier);
let root = await this.openmct.objects.getRoot();
let rootChildCount = root.composition.length;
// if not multiple root children, remove root from path
if (rootChildCount < 2) {
newObjectPath.pop(); // remove ROOT
}
this.navigateTo(newObjectPath);
}
this.showForm(object, this.oldParent);
}
inNavigationPath(object) {
@@ -96,42 +59,86 @@ export default class MoveAction {
compositionCollection.add(child);
}
removeFromOldParent(parent, child) {
let compositionCollection = this.openmct.composition.get(parent);
async onSave(object, changes, parent) {
let inNavigationPath = this.inNavigationPath(object);
if (inNavigationPath && this.openmct.editor.isEditing()) {
this.openmct.editor.save();
}
if (this.openmct.objects.areIdsEqual(parent.identifier, this.oldParent.identifier)) {
this.openmct.notifications.error(`Error: new location cant not be same as old`);
return;
}
if (changes.name && (changes.name !== object.name)) {
object.name = changes.name;
}
this.addToNewParent(object, parent);
this.removeFromOldParent(object);
if (inNavigationPath) {
let newObjectPath = await this.openmct.objects.getOriginalPath(object.identifier);
let root = await this.openmct.objects.getRoot();
let rootChildCount = root.composition.length;
// if not multiple root children, remove root from path
if (rootChildCount < 2) {
newObjectPath.pop(); // remove ROOT
}
this.navigateTo(newObjectPath);
}
}
removeFromOldParent(child) {
let compositionCollection = this.openmct.composition.get(this.oldParent);
compositionCollection.remove(child);
}
getDialogForm(object, parent) {
return {
name: "Move Item",
showForm(domainObject, parentDomainObject) {
const formStructure = {
title: "Move Item",
sections: [
{
rows: [
{
key: "name",
control: "textfield",
name: "Name",
name: "Title",
pattern: "\\S+",
required: true,
cssClass: "l-input-lg"
cssClass: "l-input-lg",
value: domainObject.name
},
{
name: "Location",
name: "location",
control: "locator",
validate: this.validate(object, parent),
required: true,
validate: this.validate(parentDomainObject),
key: 'location'
}
]
}
]
};
this.openmct.forms.showForm(formStructure, {
domainObject,
parentDomainObject,
onSave: this.onSave.bind(this)
});
}
validate(object, currentParent) {
return (parentCandidate) => {
validate(currentParent) {
return (object, data) => {
const parentCandidate = data.value;
console.log('move action : validateLocation', );
// TODO: remove getModel, checkPolicy and useCapability
let currentParentKeystring = this.openmct.objects.makeKeyString(currentParent.identifier);
let parentCandidateKeystring = this.openmct.objects.makeKeyString(parentCandidate.getId());
let parentCandidateKeystring = this.openmct.objects.makeKeyString(parentCandidate.identifier);
let objectKeystring = this.openmct.objects.makeKeyString(object.identifier);
if (!parentCandidateKeystring || !currentParentKeystring) {
@@ -146,14 +153,12 @@ export default class MoveAction {
return false;
}
if (parentCandidate.getModel().composition.indexOf(objectKeystring) !== -1) {
const parentCandidateComposition = parentCandidate.composition;
if (parentCandidateComposition && parentCandidateComposition.indexOf(objectKeystring) !== -1) {
return false;
}
return this.openmct.composition.checkPolicy(
parentCandidate.useCapability('adapter'),
object
);
return parentCandidate && this.openmct.composition.checkPolicy(parentCandidate, object);
};
}

View File

@@ -19,6 +19,8 @@
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import MoveActionPlugin from './plugin.js';
import MoveAction from './MoveAction.js';
import {
createOpenMct,
resetApplicationState,
@@ -35,6 +37,10 @@ describe("The Move Action plugin", () => {
// this setups up the app
beforeEach((done) => {
const appHolder = document.createElement('div');
appHolder.style.width = '640px';
appHolder.style.height = '480px';
openmct = createOpenMct();
childObject = getMockObjects({
@@ -67,10 +73,11 @@ describe("The Move Action plugin", () => {
}
}).folder;
openmct.on('start', done);
openmct.startHeadless();
// already installed by default, but never hurts, just adds to context menu
openmct.install(MoveActionPlugin());
moveAction = openmct.actions._allActions.move;
openmct.on('start', done);
openmct.startHeadless(appHolder);
});
afterEach(() => {
@@ -78,12 +85,13 @@ describe("The Move Action plugin", () => {
});
it("should be defined", () => {
expect(moveAction).toBeDefined();
expect(MoveActionPlugin).toBeDefined();
});
describe("when moving an object to a new parent and removing from the old parent", () => {
beforeEach(() => {
moveAction = new MoveAction(openmct);
moveAction.addToNewParent(childObject, anotherParentObject);
moveAction.removeFromOldParent(parentObject, childObject);
});

View File

@@ -79,7 +79,7 @@ describe("the plugin", () => {
spyOn(compositionAPI, 'get').and.returnValue(mockComposition);
spyOn(openmct.objects, 'save').and.returnValue(Promise.resolve(true));
return newFolderAction.invoke(mockObjectPath);
newFolderAction.invoke(mockObjectPath);
});
it('gets user input for folder name', () => {

View File

@@ -31,7 +31,7 @@
</div>
<SearchResults v-if="search.length"
ref="searchResults"
:domain-object="domainObject"
:domain-object="internalDomainObject"
:results="searchResults"
@changeSectionPage="changeSelectedSection"
@updateEntries="updateEntries"
@@ -43,18 +43,15 @@
class="c-notebook__nav c-sidebar c-drawer c-drawer--align-left"
:class="[{'is-expanded': showNav}, {'c-drawer--push': !sidebarCoversEntries}, {'c-drawer--overlays': sidebarCoversEntries}]"
:default-page-id="defaultPageId"
:selected-page-id="selectedPageId"
:default-section-id="defaultSectionId"
:selected-section-id="selectedSectionId"
:domain-object="domainObject"
:page-title="domainObject.configuration.pageTitle"
:section-title="domainObject.configuration.sectionTitle"
:domain-object="internalDomainObject"
:page-title="internalDomainObject.configuration.pageTitle"
:section-title="internalDomainObject.configuration.sectionTitle"
:sections="sections"
:selected-section="selectedSection"
:sidebar-covers-entries="sidebarCoversEntries"
@pagesChanged="pagesChanged"
@selectPage="selectPage"
@sectionsChanged="sectionsChanged"
@selectSection="selectSection"
@toggleNav="toggleNav"
/>
<div class="c-notebook__page-view">
@@ -64,10 +61,10 @@
></button>
<div class="c-notebook__page-view__path c-path">
<span class="c-notebook__path__section c-path__item">
{{ selectedSection ? selectedSection.name : '' }}
{{ getSelectedSection() ? getSelectedSection().name : '' }}
</span>
<span class="c-notebook__path__page c-path__item">
{{ selectedPage ? selectedPage.name : '' }}
{{ getSelectedPage() ? getSelectedPage().name : '' }}
</span>
</div>
<div class="c-notebook__page-view__controls">
@@ -118,9 +115,9 @@
<NotebookEntry v-for="entry in filteredAndSortedEntries"
:key="entry.id"
:entry="entry"
:domain-object="domainObject"
:selected-page="selectedPage"
:selected-section="selectedSection"
:domain-object="internalDomainObject"
:selected-page="getSelectedPage()"
:selected-section="getSelectedSection()"
:read-only="false"
@deleteEntry="deleteEntry"
@updateEntry="updateEntry"
@@ -155,19 +152,14 @@ export default {
SearchResults,
Sidebar
},
inject: ['openmct', 'snapshotContainer'],
props: {
domainObject: {
type: Object,
required: true
}
},
inject: ['openmct', 'domainObject', 'snapshotContainer'],
data() {
return {
selectedSectionId: this.getDefaultSectionId(),
selectedPageId: this.getDefaultPageId(),
defaultPageId: getDefaultNotebook() ? getDefaultNotebook().page.id : '',
defaultSectionId: getDefaultNotebook() ? getDefaultNotebook().section.id : '',
defaultSort: this.domainObject.configuration.defaultSort,
focusEntryId: null,
internalDomainObject: this.domainObject,
search: '',
searchResults: [],
showTime: 0,
@@ -176,15 +168,9 @@ export default {
};
},
computed: {
defaultPageId() {
return this.getDefaultPageId();
},
defaultSectionId() {
return this.getDefaultSectionId();
},
filteredAndSortedEntries() {
const filterTime = Date.now();
const pageEntries = getNotebookEntries(this.domainObject, this.selectedSection, this.selectedPage) || [];
const pageEntries = getNotebookEntries(this.internalDomainObject, this.selectedSection, this.selectedPage) || [];
const hours = parseInt(this.showTime, 10);
const filteredPageEntriesByTime = hours
@@ -199,28 +185,22 @@ export default {
return this.getPages() || [];
},
sections() {
return this.getSections();
return this.internalDomainObject.configuration.sections || [];
},
selectedPage() {
const pages = this.getPages();
const selectedPage = pages.find(page => page.id === this.selectedPageId);
if (selectedPage) {
return selectedPage;
if (!pages) {
return {};
}
if (!selectedPage && !pages.length) {
return undefined;
}
return pages[0];
return pages.find(page => page.isSelected);
},
selectedSection() {
if (!this.sections.length) {
return null;
return {};
}
return this.sections.find(section => section.id === this.selectedSectionId);
return this.sections.find(section => section.isSelected);
}
},
watch: {
@@ -230,14 +210,16 @@ export default {
},
beforeMount() {
this.getSearchResults = debounce(this.getSearchResults, 500);
this.syncUrlWithPageAndSection = debounce(this.syncUrlWithPageAndSection, 100);
},
mounted() {
this.unlisten = this.openmct.objects.observe(this.internalDomainObject, '*', this.updateInternalDomainObject);
this.formatSidebar();
this.setSectionAndPageFromUrl();
window.addEventListener('orientationchange', this.formatSidebar);
window.addEventListener('hashchange', this.setSectionAndPageFromUrl);
window.addEventListener("hashchange", this.navigateToSectionPage, false);
this.openmct.router.on('change:params', this.changeSectionPage);
this.navigateToSectionPage();
},
beforeDestroy() {
if (this.unlisten) {
@@ -245,7 +227,8 @@ export default {
}
window.removeEventListener('orientationchange', this.formatSidebar);
window.removeEventListener('hashchange', this.setSectionAndPageFromUrl);
window.removeEventListener("hashchange", this.navigateToSectionPage);
this.openmct.router.off('change:params', this.changeSectionPage);
},
updated: function () {
this.$nextTick(() => {
@@ -301,21 +284,14 @@ export default {
this.sectionsChanged({ sections });
this.resetSearch();
},
setSectionAndPageFromUrl() {
let sectionId = this.getSectionIdFromUrl() || this.selectedSectionId;
let pageId = this.getPageIdFromUrl() || this.selectedPageId;
this.selectSection(sectionId);
this.selectPage(pageId);
},
createNotebookStorageObject() {
const notebookMeta = {
name: this.domainObject.name,
identifier: this.domainObject.identifier,
name: this.internalDomainObject.name,
identifier: this.internalDomainObject.identifier,
link: this.getLinktoNotebook()
};
const page = this.selectedPage;
const section = this.selectedSection;
const page = this.getSelectedPage();
const section = this.getSelectedSection();
return {
notebookMeta,
@@ -324,7 +300,8 @@ export default {
};
},
deleteEntry(entryId) {
const entryPos = getEntryPosById(entryId, this.domainObject, this.selectedSection, this.selectedPage);
const self = this;
const entryPos = getEntryPosById(entryId, this.internalDomainObject, this.selectedSection, this.selectedPage);
if (entryPos === -1) {
this.openmct.notifications.alert('Warning: unable to delete entry');
console.error(`unable to delete entry ${entryId} from section ${this.selectedSection}, page ${this.selectedPage}`);
@@ -340,9 +317,9 @@ export default {
label: "Ok",
emphasis: true,
callback: () => {
const entries = getNotebookEntries(this.domainObject, this.selectedSection, this.selectedPage);
const entries = getNotebookEntries(self.internalDomainObject, self.selectedSection, self.selectedPage);
entries.splice(entryPos, 1);
this.updateEntries(entries);
self.updateEntries(entries);
dialog.dismiss();
}
},
@@ -418,37 +395,6 @@ export default {
const sidebarCoversEntries = (isPhone || (isTablet && isPortrait) || isInLayout);
this.sidebarCoversEntries = sidebarCoversEntries;
},
getDefaultPageId() {
let defaultPageId;
if (this.isDefaultNotebook()) {
defaultPageId = getDefaultNotebook().page.id;
} else {
const firstSection = this.getSections()[0];
defaultPageId = firstSection && firstSection.pages[0].id;
}
return defaultPageId;
},
isDefaultNotebook() {
const defaultNotebook = getDefaultNotebook();
const defaultNotebookIdentifier = defaultNotebook && defaultNotebook.notebookMeta.identifier;
return defaultNotebookIdentifier !== null
&& this.openmct.objects.areIdsEqual(defaultNotebookIdentifier, this.domainObject.identifier);
},
getDefaultSectionId() {
let defaultSectionId;
if (this.isDefaultNotebook()) {
defaultSectionId = getDefaultNotebook().section.id;
} else {
const firstSection = this.getSections()[0];
defaultSectionId = firstSection && firstSection.id;
}
return defaultSectionId;
},
getDefaultNotebookObject() {
const oldNotebookStorage = getDefaultNotebook();
if (!oldNotebookStorage) {
@@ -477,17 +423,14 @@ export default {
getSection(id) {
return this.sections.find(s => s.id === id);
},
getSections() {
return this.domainObject.configuration.sections || [];
},
getSearchResults() {
if (!this.search.length) {
return [];
}
const output = [];
const sections = this.domainObject.configuration.sections;
const entries = this.domainObject.configuration.entries;
const sections = this.internalDomainObject.configuration.sections;
const entries = this.internalDomainObject.configuration.entries;
const searchTextLower = this.search.toLowerCase();
const originalSearchText = this.search;
let sectionTrackPageHit;
@@ -566,25 +509,77 @@ export default {
this.searchResults = output;
},
getPages() {
const selectedSection = this.selectedSection;
const selectedSection = this.getSelectedSection();
if (!selectedSection || !selectedSection.pages.length) {
return [];
}
return selectedSection.pages;
},
getSelectedPage() {
const pages = this.getPages();
if (!pages) {
return null;
}
const selectedPage = pages.find(page => page.isSelected);
if (selectedPage) {
return selectedPage;
}
if (!selectedPage && !pages.length) {
return null;
}
pages[0].isSelected = true;
return pages[0];
},
getSelectedSection() {
if (!this.sections.length) {
return null;
}
return this.sections.find(section => section.isSelected);
},
navigateToSectionPage() {
let { pageId, sectionId } = this.openmct.router.getParams();
if (!pageId || !sectionId) {
sectionId = this.selectedSection.id;
pageId = this.selectedPage.id;
}
const sections = this.sections.map(s => {
s.isSelected = false;
if (s.id === sectionId) {
s.isSelected = true;
s.pages.forEach(p => p.isSelected = (p.id === pageId));
}
return s;
});
const selectedSectionId = this.selectedSection && this.selectedSection.id;
const selectedPageId = this.selectedPage && this.selectedPage.id;
if (selectedPageId === pageId && selectedSectionId === sectionId) {
return;
}
this.sectionsChanged({ sections });
},
newEntry(embed = null) {
this.resetSearch();
const notebookStorage = this.createNotebookStorageObject();
this.updateDefaultNotebook(notebookStorage);
const id = addNotebookEntry(this.openmct, this.domainObject, notebookStorage, embed);
const id = addNotebookEntry(this.openmct, this.internalDomainObject, notebookStorage, embed);
this.focusEntryId = id;
},
orientationChange() {
this.formatSidebar();
},
pagesChanged({ pages = [], id = null}) {
const selectedSection = this.selectedSection;
const selectedSection = this.getSelectedSection();
if (!selectedSection) {
return;
}
@@ -599,6 +594,7 @@ export default {
});
this.sectionsChanged({ sections });
this.updateDefaultNotebookPage(pages, id);
},
removeDefaultClass(domainObject) {
if (!domainObject) {
@@ -617,10 +613,10 @@ export default {
async updateDefaultNotebook(notebookStorage) {
const defaultNotebookObject = await this.getDefaultNotebookObject();
if (!defaultNotebookObject) {
setDefaultNotebook(this.openmct, notebookStorage, this.domainObject);
setDefaultNotebook(this.openmct, notebookStorage, this.internalDomainObject);
} else if (objectUtils.makeKeyString(defaultNotebookObject.identifier) !== objectUtils.makeKeyString(notebookStorage.notebookMeta.identifier)) {
this.removeDefaultClass(defaultNotebookObject);
setDefaultNotebook(this.openmct, notebookStorage, this.domainObject);
setDefaultNotebook(this.openmct, notebookStorage, this.internalDomainObject);
}
if (this.defaultSectionId && this.defaultSectionId.length === 0 || this.defaultSectionId !== notebookStorage.section.id) {
@@ -640,7 +636,7 @@ export default {
const notebookStorage = getDefaultNotebook();
if (!notebookStorage
|| notebookStorage.notebookMeta.identifier.key !== this.domainObject.identifier.key) {
|| notebookStorage.notebookMeta.identifier.key !== this.internalDomainObject.identifier.key) {
return;
}
@@ -649,7 +645,7 @@ export default {
if (!page && defaultNotebookPage.id === id) {
this.defaultSectionId = null;
this.defaultPageId = null;
this.removeDefaultClass(this.domainObject);
this.removeDefaultClass(this.internalDomainObject);
clearDefaultNotebook();
return;
@@ -668,7 +664,7 @@ export default {
const notebookStorage = getDefaultNotebook();
if (!notebookStorage
|| notebookStorage.notebookMeta.identifier.key !== this.domainObject.identifier.key) {
|| notebookStorage.notebookMeta.identifier.key !== this.internalDomainObject.identifier.key) {
return;
}
@@ -677,7 +673,7 @@ export default {
if (!section && defaultNotebookSection.id === id) {
this.defaultSectionId = null;
this.defaultPageId = null;
this.removeDefaultClass(this.domainObject);
this.removeDefaultClass(this.internalDomainObject);
clearDefaultNotebook();
return;
@@ -690,46 +686,50 @@ export default {
setDefaultNotebookSection(section);
},
updateEntry(entry) {
const entries = getNotebookEntries(this.domainObject, this.selectedSection, this.selectedPage);
const entryPos = getEntryPosById(entry.id, this.domainObject, this.selectedSection, this.selectedPage);
const entries = getNotebookEntries(this.internalDomainObject, this.selectedSection, this.selectedPage);
const entryPos = getEntryPosById(entry.id, this.internalDomainObject, this.selectedSection, this.selectedPage);
entries[entryPos] = entry;
this.updateEntries(entries);
},
updateEntries(entries) {
const configuration = this.domainObject.configuration;
const configuration = this.internalDomainObject.configuration;
const notebookEntries = configuration.entries || {};
notebookEntries[this.selectedSection.id][this.selectedPage.id] = entries;
mutateObject(this.openmct, this.domainObject, 'configuration.entries', notebookEntries);
mutateObject(this.openmct, this.internalDomainObject, 'configuration.entries', notebookEntries);
},
getPageIdFromUrl() {
return this.openmct.router.getParams().pageId;
updateInternalDomainObject(domainObject) {
this.internalDomainObject = domainObject;
},
getSectionIdFromUrl() {
return this.openmct.router.getParams().sectionId;
},
syncUrlWithPageAndSection() {
updateParams(sections) {
const selectedSection = sections.find(s => s.isSelected);
if (!selectedSection) {
return;
}
const selectedPage = selectedSection.pages.find(p => p.isSelected);
if (!selectedPage) {
return;
}
const sectionId = selectedSection.id;
const pageId = selectedPage.id;
if (!sectionId || !pageId) {
return;
}
this.openmct.router.updateParams({
pageId: this.selectedPageId,
sectionId: this.selectedSectionId
sectionId,
pageId
});
},
sectionsChanged({ sections, id = null }) {
mutateObject(this.openmct, this.domainObject, 'configuration.sections', sections);
mutateObject(this.openmct, this.internalDomainObject, 'configuration.sections', sections);
this.updateParams(sections);
this.updateDefaultNotebookSection(sections, id);
},
selectPage(pageId) {
this.selectedPageId = pageId;
this.syncUrlWithPageAndSection();
},
selectSection(sectionId) {
this.selectedSectionId = sectionId;
const defaultPageId = this.selectedSection.pages[0].id;
this.selectPage(defaultPageId);
this.syncUrlWithPageAndSection();
}
}
};

View File

@@ -4,7 +4,7 @@
class="c-ne__embed__snap-thumb"
@click="openSnapshot()"
>
<img :src="thumbnailImage">
<img :src="embed.snapshot.src">
</div>
<div class="c-ne__embed__info">
<div class="c-ne__embed__name">
@@ -25,14 +25,11 @@
<script>
import Moment from 'moment';
import PopupMenu from './PopupMenu.vue';
import PreviewAction from '../../../ui/preview/PreviewAction';
import RemoveDialog from '../utils/removeDialog';
import PainterroInstance from '../utils/painterroInstance';
import SnapshotTemplate from './snapshot-template.html';
import { updateNotebookImageDomainObject } from '../utils/notebook-image';
import PopupMenu from './PopupMenu.vue';
import Vue from 'vue';
export default {
@@ -62,11 +59,6 @@ export default {
computed: {
createdOn() {
return this.formatTime(this.embed.createdOn, 'YYYY-MM-DD HH:mm:ss');
},
thumbnailImage() {
return this.embed.snapshot.thumbnailImage
? this.embed.snapshot.thumbnailImage.src
: this.embed.snapshot.src;
}
},
mounted() {
@@ -93,7 +85,7 @@ export default {
template: '<div id="snap-annotation"></div>'
}).$mount();
const painterroInstance = new PainterroInstance(annotateVue.$el);
const painterroInstance = new PainterroInstance(annotateVue.$el, this.updateSnapshot);
const annotateOverlay = this.openmct.overlays.overlay({
element: annotateVue.$el,
size: 'large',
@@ -110,12 +102,10 @@ export default {
{
label: 'Save',
callback: () => {
painterroInstance.save((snapshotObject) => {
annotateOverlay.dismiss();
this.snapshotOverlay.dismiss();
this.updateSnapshot(snapshotObject);
this.openSnapshotOverlay(snapshotObject.fullSizeImage.src);
});
painterroInstance.save();
annotateOverlay.dismiss();
this.snapshotOverlay.dismiss();
this.openSnapshot();
}
}
],
@@ -125,19 +115,7 @@ export default {
});
painterroInstance.intialize();
const fullSizeImageObjectIdentifier = this.embed.snapshot.fullSizeImageObjectIdentifier;
if (!fullSizeImageObjectIdentifier) {
// legacy image data stored in embed
painterroInstance.show(this.embed.snapshot.src);
return;
}
this.openmct.objects.get(fullSizeImageObjectIdentifier)
.then(object => {
painterroInstance.show(object.configuration.fullSizeImageURL);
});
painterroInstance.show(this.embed.snapshot.src);
},
changeLocation() {
const hash = this.embed.historicLink;
@@ -181,29 +159,12 @@ export default {
removeDialog.show();
},
openSnapshot() {
const fullSizeImageObjectIdentifier = this.embed.snapshot.fullSizeImageObjectIdentifier;
if (!fullSizeImageObjectIdentifier) {
// legacy image data stored in embed
this.openSnapshotOverlay(this.embed.snapshot.src);
return;
}
this.openmct.objects.get(fullSizeImageObjectIdentifier)
.then(object => {
this.openSnapshotOverlay(object.configuration.fullSizeImageURL);
});
},
openSnapshotOverlay(src) {
const self = this;
this.snapshot = new Vue({
data: () => {
return {
createdOn: this.createdOn,
name: this.embed.name,
cssClass: this.embed.cssClass,
src
embed: this.embed
};
},
methods: {
@@ -256,9 +217,7 @@ export default {
this.$emit('updateEmbed', embed);
},
updateSnapshot(snapshotObject) {
this.embed.snapshot.thumbnailImage = snapshotObject.thumbnailImage;
updateNotebookImageDomainObject(this.openmct, this.embed.snapshot.fullSizeImageObjectIdentifier, snapshotObject.fullSizeImage);
this.embed.snapshot = snapshotObject;
this.updateEmbed(this.embed);
}
}

View File

@@ -62,6 +62,7 @@
<NotebookEmbed v-for="embed in entry.embeds"
:key="embed.id"
:embed="embed"
:entry="entry"
@removeEmbed="removeEmbed"
@updateEmbed="updateEmbed"
/>
@@ -253,7 +254,6 @@ export default {
},
removeEmbed(id) {
const embedPosition = this.findPositionInArray(this.entry.embeds, id);
// TODO: remove notebook snapshot object using object remove API
this.entry.embeds.splice(embedPosition, 1);
this.$emit('updateEntry', this.entry);

View File

@@ -6,7 +6,6 @@
>
<Page ref="pageComponent"
:default-page-id="defaultPageId"
:selected-page-id="selectedPageId"
:page="page"
:page-title="pageTitle"
@deletePage="deletePage"
@@ -34,13 +33,11 @@ export default {
return '';
}
},
selectedPageId: {
type: String,
required: true
},
domainObject: {
type: Object,
required: true
default() {
return {};
}
},
pages: {
type: Array,
@@ -69,17 +66,7 @@ export default {
}
}
},
watch: {
pages() {
if (!this.containsPage(this.selectedPageId)) {
this.selectPage(this.pages[0].id);
}
}
},
methods: {
containsPage(pageId) {
return this.pages.some(page => page.id === pageId);
},
deletePage(id) {
const selectedSection = this.sections.find(s => s.isSelected);
const page = this.pages.find(p => p.id === id);
@@ -91,29 +78,37 @@ export default {
const isPageSelected = selectedPage && selectedPage.id === id;
const isPageDefault = defaultpage && defaultpage.id === id;
const pages = this.pages.filter(s => s.id !== id);
let selectedPageId;
if (isPageSelected && defaultpage) {
pages.forEach(s => {
s.isSelected = false;
if (defaultpage && defaultpage.id === s.id) {
selectedPageId = s.id;
s.isSelected = true;
}
});
}
if (pages.length && isPageSelected && (!defaultpage || isPageDefault)) {
selectedPageId = pages[0].id;
pages[0].isSelected = true;
}
this.$emit('updatePage', {
pages,
id
});
this.$emit('selectPage', selectedPageId);
},
selectPage(id) {
this.$emit('selectPage', id);
const pages = this.pages.map(page => {
const isSelected = page.id === id;
page.isSelected = isSelected;
return page;
});
this.$emit('updatePage', {
pages,
id
});
// Add test here for whether or not to toggle the nav
if (this.sidebarCoversEntries) {

View File

@@ -1,6 +1,6 @@
<template>
<div class="c-list__item js-list__item"
:class="[{ 'is-selected': isSelected, 'is-notebook-default' : (defaultPageId === page.id) }]"
:class="[{ 'is-selected': page.isSelected, 'is-notebook-default' : (defaultPageId === page.id) }]"
:data-id="page.id"
@click="selectPage"
>
@@ -29,10 +29,6 @@ export default {
return '';
}
},
selectedPageId: {
type: String,
required: true
},
page: {
type: Object,
required: true
@@ -50,11 +46,6 @@ export default {
removeActionString: `Delete ${this.pageTitle}`
};
},
computed: {
isSelected() {
return this.selectedPageId === this.page.id;
}
},
watch: {
page(newPage) {
this.toggleContentEditable(newPage);
@@ -82,7 +73,7 @@ export default {
this.$emit('deletePage', this.page.id);
},
getRemoveDialog() {
const message = 'Other users may be editing entries in this page, and deleting it is permanent. Do you want to continue?';
const message = 'This action will delete this page and all of its entries. Do you want to continue?';
const options = {
name: this.removeActionString,
callback: this.deletePage.bind(this),

View File

@@ -4,14 +4,13 @@
:key="section.id"
class="c-list__item-h"
>
<NotebookSection ref="sectionComponent"
:default-section-id="defaultSectionId"
:selected-section-id="selectedSectionId"
:section="section"
:section-title="sectionTitle"
@deleteSection="deleteSection"
@renameSection="updateSection"
@selectSection="selectSection"
<sectionComponent ref="sectionComponent"
:default-section-id="defaultSectionId"
:section="section"
:section-title="sectionTitle"
@deleteSection="deleteSection"
@renameSection="updateSection"
@selectSection="selectSection"
/>
</li>
</ul>
@@ -20,11 +19,11 @@
<script>
import { deleteNotebookEntries } from '../utils/notebook-entries';
import { getDefaultNotebook } from '../utils/notebook-storage';
import SectionComponent from './SectionComponent.vue';
import sectionComponent from './SectionComponent.vue';
export default {
components: {
NotebookSection: SectionComponent
sectionComponent
},
inject: ['openmct'],
props: {
@@ -34,10 +33,6 @@ export default {
return '';
}
},
selectedSectionId: {
type: String,
required: true
},
domainObject: {
type: Object,
default() {
@@ -58,22 +53,12 @@ export default {
}
}
},
watch: {
sections() {
if (!this.containsSection(this.selectedSectionId)) {
this.selectSection(this.sections[0].id);
}
}
},
methods: {
containsSection(sectionId) {
return this.sections.some(section => section.id === sectionId);
},
deleteSection(id) {
const section = this.sections.find(s => s.id === id);
deleteNotebookEntries(this.openmct, this.domainObject, section);
const selectedSection = this.sections.find(s => s.id === this.selectedSectionId);
const selectedSection = this.sections.find(s => s.isSelected);
const defaultNotebook = getDefaultNotebook();
const defaultSection = defaultNotebook && defaultNotebook.section;
const isSectionSelected = selectedSection && selectedSection.id === id;
@@ -98,8 +83,18 @@ export default {
id
});
},
selectSection(id) {
this.$emit('selectSection', id);
selectSection(id, newSections) {
const currentSections = newSections || this.sections;
const sections = currentSections.map(section => {
const isSelected = section.id === id;
section.isSelected = isSelected;
return section;
});
this.$emit('updateSection', {
sections,
id
});
},
updateSection(newSection) {
const id = newSection.id;

View File

@@ -1,6 +1,6 @@
<template>
<div class="c-list__item js-list__item"
:class="[{ 'is-selected': isSelected, 'is-notebook-default' : (defaultSectionId === section.id) }]"
:class="[{ 'is-selected': section.isSelected, 'is-notebook-default' : (defaultSectionId === section.id) }]"
:data-id="section.id"
@click="selectSection"
>
@@ -13,6 +13,9 @@
</div>
</template>
<style lang="scss">
</style>
<script>
import PopupMenu from './PopupMenu.vue';
import RemoveDialog from '../utils/removeDialog';
@@ -29,10 +32,6 @@ export default {
return '';
}
},
selectedSectionId: {
type: String,
required: true
},
section: {
type: Object,
required: true
@@ -50,11 +49,6 @@ export default {
removeActionString: `Delete ${this.sectionTitle}`
};
},
computed: {
isSelected() {
return this.selectedSectionId === this.section.id;
}
},
watch: {
section(newSection) {
this.toggleContentEditable(newSection);
@@ -82,7 +76,7 @@ export default {
this.$emit('deleteSection', this.section.id);
},
getRemoveDialog() {
const message = 'Other users may be editing entries in this section, and deleting it is permanent. Do you want to continue?';
const message = 'This action will delete this section and all of its pages and entries. Do you want to continue?';
const options = {
name: this.removeActionString,
callback: this.deleteSection.bind(this),

View File

@@ -1,6 +1,6 @@
<template>
<div class="c-sidebar c-drawer c-drawer--align-left">
<div class="c-sidebar__pane js-sidebar-sections">
<div class="c-sidebar__pane">
<div class="c-sidebar__header-w">
<div class="c-sidebar__header">
<span class="c-sidebar__header-label">{{ sectionTitle }}</span>
@@ -15,16 +15,14 @@
</button>
<SectionCollection class="c-sidebar__contents"
:default-section-id="defaultSectionId"
:selected-section-id="selectedSectionId"
:domain-object="domainObject"
:sections="sections"
:section-title="sectionTitle"
@updateSection="sectionsChanged"
@selectSection="selectSection"
/>
</div>
</div>
<div class="c-sidebar__pane js-sidebar-pages">
<div class="c-sidebar__pane">
<div class="c-sidebar__header-w">
<div class="c-sidebar__header">
<span class="c-sidebar__header-label">{{ pageTitle }}</span>
@@ -44,7 +42,6 @@
<PageCollection ref="pageCollection"
class="c-sidebar__contents"
:default-page-id="defaultPageId"
:selected-page-id="selectedPageId"
:domain-object="domainObject"
:pages="pages"
:sections="sections"
@@ -52,7 +49,6 @@
:page-title="pageTitle"
@toggleNav="toggleNav"
@updatePage="pagesChanged"
@selectPage="selectPage"
/>
</div>
</div>
@@ -77,24 +73,12 @@ export default {
return '';
}
},
selectedPageId: {
type: String,
default() {
return '';
}
},
defaultSectionId: {
type: String,
default() {
return '';
}
},
selectedSectionId: {
type: String,
default() {
return '';
}
},
domainObject: {
type: Object,
default() {
@@ -129,7 +113,7 @@ export default {
},
computed: {
pages() {
const selectedSection = this.sections.find(section => section.id === this.selectedSectionId);
const selectedSection = this.sections.find(section => section.isSelected);
return selectedSection && selectedSection.pages || [];
}
@@ -160,7 +144,6 @@ export default {
pages,
id: newPage.id
});
this.$emit('selectPage', newPage.id);
},
addSection() {
const newSection = this.createNewSection();
@@ -170,8 +153,6 @@ export default {
sections,
id: newSection.id
});
this.$emit('selectSection', newSection.id);
},
addNewPage(page) {
const pages = this.pages.map(p => {
@@ -227,12 +208,6 @@ export default {
id
});
},
selectPage(pageId) {
this.$emit('selectPage', pageId);
},
selectSection(sectionId) {
this.$emit('selectSection', sectionId);
},
sectionsChanged({ sections, id }) {
this.$emit('sectionsChanged', {
sections,

View File

@@ -4,9 +4,9 @@
<div class="l-browse-bar__start">
<div class="l-browse-bar__object-name--w">
<span class="c-object-label l-browse-bar__object-name"
v-bind:class="cssClass"
v-bind:class="embed.cssClass"
>
<span class="c-object-label__name">{{ name }}</span>
<span class="c-object-label__name">{{ embed.name }}</span>
</span>
</div>
</div>
@@ -40,7 +40,7 @@
<div
ref="snapshot-image"
class="c-notebook-snapshot__image"
:style="{ backgroundImage: 'url(' + src + ')' }"
:style="{ backgroundImage: 'url(' + embed.snapshot.src + ')' }"
>
</div>
</div>

View File

@@ -1,4 +1,3 @@
export const NOTEBOOK_TYPE = 'notebook';
export const EVENT_SNAPSHOTS_UPDATED = 'SNAPSHOTS_UPDATED';
export const NOTEBOOK_DEFAULT = 'DEFAULT';
export const NOTEBOOK_SNAPSHOT = 'SNAPSHOT';

View File

@@ -2,20 +2,18 @@ import CopyToNotebookAction from './actions/CopyToNotebookAction';
import Notebook from './components/Notebook.vue';
import NotebookSnapshotIndicator from './components/NotebookSnapshotIndicator.vue';
import SnapshotContainer from './snapshot-container';
import { notebookImageMigration } from '../notebook/utils/notebook-migration';
import { NOTEBOOK_TYPE } from './notebook-constants';
import Vue from 'vue';
let installed = false;
export default function NotebookPlugin() {
return function install(openmct) {
if (openmct._NOTEBOOK_PLUGIN_INSTALLED) {
if (installed) {
return;
} else {
openmct._NOTEBOOK_PLUGIN_INSTALLED = true;
}
installed = true;
openmct.actions.register(new CopyToNotebookAction(openmct));
const notebookType = {
@@ -86,20 +84,7 @@ export default function NotebookPlugin() {
}
]
};
openmct.types.addType(NOTEBOOK_TYPE, notebookType);
const notebookSnapshotImageType = {
name: 'Notebook Snapshot Image Storage',
description: 'Notebook Snapshot Image Storage object',
creatable: false,
initialize: domainObject => {
domainObject.configuration = {
fullSizeImageURL: undefined,
thumbnailImageURL: undefined
};
}
};
openmct.types.addType('notebookSnapshotImage', notebookSnapshotImageType);
openmct.types.addType('notebook', notebookType);
const snapshotContainer = new SnapshotContainer(openmct);
const notebookSnapshotIndicator = new Vue ({
@@ -138,14 +123,10 @@ export default function NotebookPlugin() {
},
provide: {
openmct,
domainObject,
snapshotContainer
},
data() {
return {
domainObject
};
},
template: '<Notebook :domain-object="domainObject"></Notebook>'
template: '<Notebook></Notebook>'
});
},
destroy() {
@@ -154,16 +135,5 @@ export default function NotebookPlugin() {
};
}
});
openmct.objects.addGetInterceptor({
appliesTo: (identifier, domainObject) => {
return domainObject && domainObject.type === 'notebook';
},
invoke: (identifier, domainObject) => {
notebookImageMigration(openmct, domainObject);
return domainObject;
}
});
};
}

View File

@@ -21,32 +21,29 @@
*****************************************************************************/
import { createOpenMct, createMouseEvent, resetApplicationState } from 'utils/testing';
import notebookPlugin from './plugin';
import NotebookPlugin from './plugin';
import Vue from 'vue';
let openmct;
let notebookDefinition;
let notebookPlugin;
let element;
let child;
let appHolder;
const notebookDomainObject = {
identifier: {
key: 'notebook',
namespace: ''
},
type: 'notebook'
};
describe("Notebook plugin:", () => {
let openmct;
let notebookDefinition;
let element;
let child;
let appHolder;
let objectProviderObserver;
let notebookDomainObject;
beforeEach((done) => {
notebookDomainObject = {
identifier: {
key: 'notebook',
namespace: 'test-namespace'
},
type: 'notebook'
};
beforeAll(done => {
appHolder = document.createElement('div');
appHolder.style.width = '640px';
appHolder.style.height = '480px';
document.body.appendChild(appHolder);
openmct = createOpenMct();
@@ -54,16 +51,19 @@ describe("Notebook plugin:", () => {
child = document.createElement('div');
element.appendChild(child);
openmct.install(notebookPlugin());
notebookPlugin = new NotebookPlugin();
openmct.install(notebookPlugin);
notebookDefinition = openmct.types.get('notebook').definition;
notebookDefinition.initialize(notebookDomainObject);
openmct.on('start', done);
openmct.start(appHolder);
document.body.append(appHolder);
});
afterEach(() => {
afterAll(() => {
appHolder.remove();
return resetApplicationState(openmct);
@@ -80,96 +80,39 @@ describe("Notebook plugin:", () => {
describe("Notebook view:", () => {
let notebookViewProvider;
let notebookView;
let notebookViewObject;
let mutableNotebookObject;
beforeEach(() => {
notebookViewObject = {
const notebookViewObject = {
...notebookDomainObject,
id: "test-object",
name: 'Notebook',
configuration: {
defaultSort: 'oldest',
entries: {
"test-section-1": {
"test-page-1": [{
"id": "entry-0",
"createdOn": 0,
"text": "First Test Entry",
"embeds": []
}, {
"id": "entry-1",
"createdOn": 0,
"text": "Second Test Entry",
"embeds": []
}]
}
},
entries: {},
pageTitle: 'Page',
sections: [{
"id": "test-section-1",
"isDefault": false,
"isSelected": false,
"name": "Test Section",
"pages": [{
"id": "test-page-1",
"isDefault": false,
"isSelected": false,
"name": "Test Page 1",
"pageTitle": "Page"
}, {
"id": "test-page-2",
"isDefault": false,
"isSelected": false,
"name": "Test Page 2",
"pageTitle": "Page"
}]
}, {
"id": "test-section-2",
"isDefault": false,
"isSelected": false,
"name": "Test Section 2",
"pages": [{
"id": "test-page-3",
"isDefault": false,
"isSelected": false,
"name": "Test Page 3",
"pageTitle": "Page"
}]
}],
sections: [],
sectionTitle: 'Section',
type: 'General'
}
};
const testObjectProvider = jasmine.createSpyObj('testObjectProvider', [
'get',
'create',
'update',
'observe'
]);
const applicableViews = openmct.objectViews.get(notebookViewObject, [notebookViewObject]);
notebookViewProvider = applicableViews.find(viewProvider => viewProvider.key === 'notebook-vue');
const notebookObject = {
name: 'Notebook View',
key: 'notebook-vue',
creatable: true
};
testObjectProvider.get.and.returnValue(Promise.resolve(notebookViewObject));
openmct.objects.addProvider('test-namespace', testObjectProvider);
testObjectProvider.observe.and.returnValue(() => {});
const applicableViews = openmct.objectViews.get(notebookViewObject, []);
notebookViewProvider = applicableViews.find(viewProvider => viewProvider.key === notebookObject.key);
notebookView = notebookViewProvider.view(notebookViewObject);
return openmct.objects.getMutable(notebookViewObject.identifier).then((mutableObject) => {
mutableNotebookObject = mutableObject;
objectProviderObserver = testObjectProvider.observe.calls.mostRecent().args[1];
notebookView = notebookViewProvider.view(mutableNotebookObject);
notebookView.show(child);
return Vue.nextTick();
});
notebookView.show(child);
return Vue.nextTick();
});
afterEach(() => {
notebookView.destroy();
openmct.objects.destroyMutable(mutableNotebookObject);
});
it("provides notebook view", () => {
@@ -190,114 +133,6 @@ describe("Notebook plugin:", () => {
expect(hasMajorElements).toBe(true);
});
it("renders a row for each entry", () => {
const notebookEntryElements = element.querySelectorAll('.c-notebook__entry');
const firstEntryText = getEntryText(0);
expect(notebookEntryElements.length).toBe(2);
expect(firstEntryText.innerText).toBe('First Test Entry');
});
describe("synchronization", () => {
it("updates an entry when another user modifies it", () => {
expect(getEntryText(0).innerText).toBe("First Test Entry");
notebookViewObject.configuration.entries["test-section-1"]["test-page-1"][0].text = "Modified entry text";
objectProviderObserver(notebookViewObject);
return Vue.nextTick().then(() => {
expect(getEntryText(0).innerText).toBe("Modified entry text");
});
});
it("shows new entry when another user adds one", () => {
expect(allNotebookEntryElements().length).toBe(2);
notebookViewObject.configuration.entries["test-section-1"]["test-page-1"].push({
"id": "entry-3",
"createdOn": 0,
"text": "Third Test Entry",
"embeds": []
});
objectProviderObserver(notebookViewObject);
return Vue.nextTick().then(() => {
expect(allNotebookEntryElements().length).toBe(3);
});
});
it("removes an entry when another user removes one", () => {
expect(allNotebookEntryElements().length).toBe(2);
let entries = notebookViewObject.configuration.entries["test-section-1"]["test-page-1"];
notebookViewObject.configuration.entries["test-section-1"]["test-page-1"] = entries.splice(0, 1);
objectProviderObserver(notebookViewObject);
return Vue.nextTick().then(() => {
expect(allNotebookEntryElements().length).toBe(1);
});
});
it("updates the notebook when a user adds a page", () => {
const newPage = {
"id": "test-page-4",
"isDefault": false,
"isSelected": false,
"name": "Test Page 4",
"pageTitle": "Page"
};
expect(allNotebookPageElements().length).toBe(2);
notebookViewObject.configuration.sections[0].pages.push(newPage);
objectProviderObserver(notebookViewObject);
return Vue.nextTick().then(() => {
expect(allNotebookPageElements().length).toBe(3);
});
});
it("updates the notebook when a user removes a page", () => {
expect(allNotebookPageElements().length).toBe(2);
notebookViewObject.configuration.sections[0].pages.splice(0, 1);
objectProviderObserver(notebookViewObject);
return Vue.nextTick().then(() => {
expect(allNotebookPageElements().length).toBe(1);
});
});
it("updates the notebook when a user adds a section", () => {
const newSection = {
"id": "test-section-3",
"isDefault": false,
"isSelected": false,
"name": "Test Section 3",
"pages": [{
"id": "test-page-4",
"isDefault": false,
"isSelected": false,
"name": "Test Page 4",
"pageTitle": "Page"
}]
};
expect(allNotebookSectionElements().length).toBe(2);
notebookViewObject.configuration.sections.push(newSection);
objectProviderObserver(notebookViewObject);
return Vue.nextTick().then(() => {
expect(allNotebookSectionElements().length).toBe(3);
});
});
it("updates the notebook when a user removes a section", () => {
expect(allNotebookSectionElements().length).toBe(2);
notebookViewObject.configuration.sections.splice(0, 1);
objectProviderObserver(notebookViewObject);
return Vue.nextTick().then(() => {
expect(allNotebookSectionElements().length).toBe(1);
});
});
});
});
describe("Notebook Snapshots view:", () => {
@@ -312,22 +147,16 @@ describe("Notebook plugin:", () => {
button.dispatchEvent(clickEvent);
}
beforeEach(() => {
beforeAll(() => {
snapshotIndicator = openmct.indicators.indicatorObjects
.find(indicator => indicator.key === 'notebook-snapshot-indicator').element;
element.append(snapshotIndicator);
return Vue.nextTick().then(() => {
drawerElement = document.querySelector('.l-shell__drawer');
});
return Vue.nextTick();
});
afterEach(() => {
if (drawerElement) {
drawerElement.classList.remove('is-expanded');
}
afterAll(() => {
snapshotIndicator.remove();
snapshotIndicator = undefined;
@@ -337,6 +166,16 @@ describe("Notebook plugin:", () => {
}
});
beforeEach(() => {
drawerElement = document.querySelector('.l-shell__drawer');
});
afterEach(() => {
if (drawerElement) {
drawerElement.classList.remove('is-expanded');
}
});
it("has Snapshots indicator", () => {
const hasSnapshotIndicator = snapshotIndicator !== null && snapshotIndicator !== undefined;
expect(hasSnapshotIndicator).toBe(true);
@@ -380,20 +219,4 @@ describe("Notebook plugin:", () => {
expect(snapshotsText).toBe('Notebook Snapshots');
});
});
function getEntryText(entryNumber) {
return element.querySelectorAll('.c-notebook__entry .c-ne__text')[entryNumber];
}
function allNotebookEntryElements() {
return element.querySelectorAll('.c-notebook__entry');
}
function allNotebookSectionElements() {
return element.querySelectorAll('.js-sidebar-sections .js-list__item');
}
function allNotebookPageElements() {
return element.querySelectorAll('.js-sidebar-pages .js-list__item');
}
});

View File

@@ -1,8 +1,6 @@
import { addNotebookEntry, createNewEmbed } from './utils/notebook-entries';
import { getDefaultNotebook, getDefaultNotebookLink, setDefaultNotebook } from './utils/notebook-storage';
import { NOTEBOOK_DEFAULT } from '@/plugins/notebook/notebook-constants';
import { createNotebookImageDomainObject, DEFAULT_SIZE } from './utils/notebook-image';
import SnapshotContainer from './snapshot-container';
export default class Snapshot {
@@ -16,17 +14,12 @@ export default class Snapshot {
capture(snapshotMeta, notebookType, domElement) {
const exportImageService = this.openmct.$injector.get('exportImageService');
const options = {
className: 's-status-taking-snapshot',
thumbnailSize: DEFAULT_SIZE
};
exportImageService.exportPNGtoSRC(domElement, options)
.then(function ({blob, thumbnail}) {
exportImageService.exportPNGtoSRC(domElement, 's-status-taking-snapshot')
.then(function (blob) {
const reader = new window.FileReader();
reader.readAsDataURL(blob);
reader.onloadend = function () {
this._saveSnapShot(notebookType, reader.result, thumbnail, snapshotMeta);
this._saveSnapShot(notebookType, reader.result, snapshotMeta);
}.bind(this);
}.bind(this));
}
@@ -34,23 +27,16 @@ export default class Snapshot {
/**
* @private
*/
_saveSnapShot(notebookType, fullSizeImageURL, thumbnailImageURL, snapshotMeta) {
createNotebookImageDomainObject(this.openmct, fullSizeImageURL)
.then(object => {
const thumbnailImage = { src: thumbnailImageURL || '' };
const snapshot = {
fullSizeImageObjectIdentifier: object.identifier,
thumbnailImage
};
const embed = createNewEmbed(snapshotMeta, snapshot);
if (notebookType === NOTEBOOK_DEFAULT) {
this._saveToDefaultNoteBook(embed);
_saveSnapShot(notebookType, imageUrl, snapshotMeta) {
const snapshot = imageUrl ? { src: imageUrl } : '';
const embed = createNewEmbed(snapshotMeta, snapshot);
if (notebookType === NOTEBOOK_DEFAULT) {
this._saveToDefaultNoteBook(embed);
return;
}
return;
}
this._saveToNotebookSnapshots(embed);
});
this._saveToNotebookSnapshots(embed);
}
/**

View File

@@ -112,7 +112,7 @@ let openmct;
let mockIdentifierService;
describe('Notebook Entries:', () => {
beforeEach(() => {
beforeEach(done => {
openmct = createOpenMct();
openmct.$injector = jasmine.createSpyObj('$injector', ['get']);
mockIdentifierService = jasmine.createSpyObj(
@@ -134,6 +134,8 @@ describe('Notebook Entries:', () => {
'update'
]));
window.localStorage.setItem('notebook-storage', null);
done();
});
afterEach(() => {
@@ -148,11 +150,12 @@ describe('Notebook Entries:', () => {
expect(entries.length).toEqual(0);
});
it('addNotebookEntry adds entry', () => {
it('addNotebookEntry adds entry', (done) => {
const unlisten = openmct.objects.observe(notebookDomainObject, '*', (object) => {
const entries = NotebookEntries.getNotebookEntries(notebookDomainObject, selectedSection, selectedPage);
expect(entries.length).toEqual(1);
done();
unlisten();
});

View File

@@ -1,78 +0,0 @@
import uuid from 'uuid';
export const DEFAULT_SIZE = {
width: 30,
height: 30
};
export function createNotebookImageDomainObject(openmct, fullSizeImageURL) {
const identifier = {
key: uuid(),
namespace: ''
};
const viewType = 'notebookSnapshotImage';
const object = {
name: 'Notebook Snapshot Image',
type: viewType,
identifier,
configuration: {
fullSizeImageURL
}
};
return new Promise((resolve, reject) => {
openmct.objects.save(object)
.then(result => {
if (result) {
resolve(object);
}
reject();
})
.catch(e => {
console.error(e);
reject();
});
});
}
export function getThumbnailURLFromCanvas(canvas, size = DEFAULT_SIZE) {
const thumbnailCanvas = document.createElement('canvas');
thumbnailCanvas.setAttribute('width', size.width);
thumbnailCanvas.setAttribute('height', size.height);
const ctx = thumbnailCanvas.getContext('2d');
ctx.globalCompositeOperation = "copy";
ctx.drawImage(canvas, 0, 0, canvas.width, canvas.height, 0, 0, size.width, size.height);
return thumbnailCanvas.toDataURL('image/png');
}
export function getThumbnailURLFromimageUrl(imageUrl, size = DEFAULT_SIZE) {
return new Promise(resolve => {
const image = new Image();
const canvas = document.createElement('canvas');
canvas.width = size.width;
canvas.height = size.height;
image.onload = function () {
canvas.getContext('2d')
.drawImage(image, 0, 0, size.width, size.height);
resolve(canvas.toDataURL('image/png'));
};
image.src = imageUrl;
});
}
export function updateNotebookImageDomainObject(openmct, identifier, fullSizeImage) {
openmct.objects.get(identifier)
.then(domainObject => {
const configuration = domainObject.configuration;
configuration.fullSizeImageURL = fullSizeImage.src;
openmct.objects.mutate(domainObject, 'configuration', configuration);
});
}

View File

@@ -1,43 +0,0 @@
import { createNotebookImageDomainObject, getThumbnailURLFromimageUrl } from './notebook-image';
import { mutateObject } from './notebook-entries';
export function notebookImageMigration(openmct, domainObject) {
const configuration = domainObject.configuration;
const notebookEntries = configuration.entries;
const imageMigrationVer = configuration.imageMigrationVer;
if (imageMigrationVer && imageMigrationVer === 'v1') {
return;
}
configuration.imageMigrationVer = 'v1';
// to avoid muliple notebookImageMigration calls updating images.
mutateObject(openmct, domainObject, 'configuration', configuration);
configuration.sections.forEach(section => {
const sectionId = section.id;
section.pages.forEach(page => {
const pageId = page.id;
const notebookSection = notebookEntries && notebookEntries[sectionId] || {};
const pageEntries = notebookSection && notebookSection[pageId] || [];
pageEntries.forEach(entry => {
entry.embeds.forEach(async (embed) => {
const snapshot = embed.snapshot;
const fullSizeImageURL = snapshot.src;
if (fullSizeImageURL) {
const thumbnailImageURL = await getThumbnailURLFromimageUrl(fullSizeImageURL);
const notebookImageDomainObject = await createNotebookImageDomainObject(openmct, fullSizeImageURL);
embed.snapshot = {
fullSizeImageObjectIdentifier: notebookImageDomainObject.identifier,
thumbnailImage: { src: thumbnailImageURL || '' }
};
mutateObject(openmct, domainObject, 'configuration.entries', notebookEntries);
}
});
});
});
});
}

View File

@@ -60,7 +60,7 @@ let openmct;
let mockIdentifierService;
describe('Notebook Storage:', () => {
beforeEach(() => {
beforeEach((done) => {
openmct = createOpenMct();
openmct.$injector = jasmine.createSpyObj('$injector', ['get']);
mockIdentifierService = jasmine.createSpyObj(
@@ -79,6 +79,7 @@ describe('Notebook Storage:', () => {
'create',
'update'
]));
done();
});
afterEach(() => {

View File

@@ -1,5 +1,4 @@
import Painterro from 'painterro';
import { getThumbnailURLFromimageUrl } from './notebook-image';
const DEFAULT_CONFIG = {
activeColor: '#ff0000',
@@ -26,11 +25,11 @@ const DEFAULT_CONFIG = {
};
export default class PainterroInstance {
constructor(element) {
constructor(element, saveCallback) {
this.elementId = element.id;
this.isSave = false;
this.painterroInstance = undefined;
this.saveCallback = undefined;
this.painterroInstance = null;
this.saveCallback = saveCallback;
}
dismiss() {
@@ -47,41 +46,31 @@ export default class PainterroInstance {
this.painterro = Painterro(this.config);
}
save(callback) {
this.saveCallback = callback;
save() {
this.isSave = true;
this.painterroInstance.save();
}
saveHandler(image, done) {
if (this.isSave) {
const self = this;
const url = image.asBlob();
const reader = new window.FileReader();
reader.readAsDataURL(url);
reader.onloadend = async () => {
const fullSizeImageURL = reader.result;
const thumbnailURL = await getThumbnailURLFromimageUrl(fullSizeImageURL);
reader.onloadend = () => {
const snapshot = reader.result;
const snapshotObject = {
fullSizeImage: {
src: fullSizeImageURL,
type: url.type,
size: url.size,
modified: Date.now()
},
thumbnailImage: {
src: thumbnailURL,
modified: Date.now()
}
src: snapshot,
type: url.type,
size: url.size,
modified: Date.now()
};
this.saveCallback(snapshotObject);
done(true);
self.saveCallback(snapshotObject);
};
} else {
done(true);
}
done(true);
}
show(src) {

View File

@@ -1,75 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, 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,
spyOnBuiltins
} from 'utils/testing';
describe("the plugin", () => {
let openmct;
let openInNewTabAction;
let mockObjectPath;
beforeEach((done) => {
openmct = createOpenMct();
openmct.on('start', done);
openmct.startHeadless();
openInNewTabAction = openmct.actions._allActions.newTab;
});
afterEach(() => {
return resetApplicationState(openmct);
});
it('installs the open in new tab action', () => {
expect(openInNewTabAction).toBeDefined();
});
describe('when invoked', () => {
beforeEach(async () => {
mockObjectPath = [{
name: 'mock folder',
type: 'folder',
identifier: {
key: 'mock-folder',
namespace: ''
}
}];
spyOn(openmct.objects, 'get').and.returnValue(Promise.resolve({
identifier: {
namespace: '',
key: 'test'
}
}));
spyOnBuiltins(['open']);
await openInNewTabAction.invoke(mockObjectPath);
});
it('it opens in a new tab', () => {
expect(window.open).toHaveBeenCalled();
});
});
});

View File

@@ -22,7 +22,6 @@
import CouchDocument from "./CouchDocument";
import CouchObjectQueue from "./CouchObjectQueue";
import { NOTEBOOK_TYPE } from '../../notebook/notebook-constants.js';
const REV = "_rev";
const ID = "_id";
@@ -30,14 +29,24 @@ const HEARTBEAT = 50000;
const ALL_DOCS = "_all_docs?include_docs=true";
export default class CouchObjectProvider {
// options {
// url: couchdb url,
// disableObserve: disable auto feed from couchdb to keep objects in sync,
// filter: selector to find objects to sync in couchdb
// }
constructor(openmct, options, namespace) {
options = this._normalize(options);
this.openmct = openmct;
this.url = options.url;
this.namespace = namespace;
this.objectQueue = {};
this.observeEnabled = options.disableObserve !== true;
this.observers = {};
this.batchIds = [];
if (this.observeEnabled) {
this.observeObjectChanges(options.filter);
}
}
//backwards compatibility, options used to be a url. Now it's an object
@@ -124,12 +133,8 @@ export default class CouchObjectProvider {
this.objectQueue[key] = new CouchObjectQueue(undefined, response[REV]);
}
if (object.type === NOTEBOOK_TYPE) {
//Temporary measure until object sync is supported for all object types
//Always update notebook revision number because we have realtime sync, so always assume it's the latest.
this.objectQueue[key].updateRevision(response[REV]);
} else if (!this.objectQueue[key].pending) {
//Sometimes CouchDB returns the old rev which fetching the object if there is a document update in progress
//Sometimes CouchDB returns the old rev which fetching the object if there is a document update in progress
if (!this.objectQueue[key].pending) {
this.objectQueue[key].updateRevision(response[REV]);
}
@@ -308,63 +313,49 @@ export default class CouchObjectProvider {
}
observe(identifier, callback) {
if (!this.observeEnabled) {
return;
}
const keyString = this.openmct.objects.makeKeyString(identifier);
this.observers[keyString] = this.observers[keyString] || [];
this.observers[keyString].push(callback);
if (!this.isObservingObjectChanges()) {
this.observeObjectChanges();
}
return () => {
this.observers[keyString] = this.observers[keyString].filter(observer => observer !== callback);
if (this.observers[keyString].length === 0) {
delete this.observers[keyString];
if (Object.keys(this.observers).length === 0) {
this.stopObservingObjectChanges();
}
}
};
}
isObservingObjectChanges() {
return this.stopObservingObjectChanges !== undefined;
}
/**
* @private
*/
async observeObjectChanges() {
abortGetChanges() {
if (this.controller) {
this.controller.abort();
this.controller = undefined;
}
return true;
}
/**
* @private
*/
async observeObjectChanges(filter) {
let intermediateResponse = this.getIntermediateResponse();
if (!this.observeEnabled) {
intermediateResponse.reject('Observe for changes is disabled');
}
const controller = new AbortController();
const signal = controller.signal;
let filter = {selector: {}};
if (this.openmct.objects.SYNCHRONIZED_OBJECT_TYPES.length > 1) {
filter.selector.$or = this.openmct.objects.SYNCHRONIZED_OBJECT_TYPES
.map(type => {
return {
'model': {
type
}
};
});
} else {
filter.selector.model = {
type: this.openmct.objects.SYNCHRONIZED_OBJECT_TYPES[0]
};
if (this.controller) {
this.abortGetChanges();
}
let error = false;
if (typeof this.stopObservingObjectChanges === 'function') {
this.stopObservingObjectChanges();
}
this.stopObservingObjectChanges = () => {
controller.abort();
delete this.stopObservingObjectChanges;
};
this.controller = controller;
// feed=continuous maintains an indefinitely open connection with a keep-alive of HEARTBEAT milliseconds until this client closes the connection
// style=main_only returns only the current winning revision of the document
let url = `${this.url}/_changes?feed=continuous&style=main_only&heartbeat=${HEARTBEAT}`;
@@ -383,20 +374,14 @@ export default class CouchObjectProvider {
},
body
});
const reader = response.body.getReader();
let completed = false;
let reader;
if (response.body === undefined) {
error = true;
} else {
reader = response.body.getReader();
}
while (!error) {
while (!completed) {
const {done, value} = await reader.read();
//done is true when we lose connection with the provider
if (done) {
error = true;
completed = true;
}
if (value) {
@@ -429,9 +414,11 @@ export default class CouchObjectProvider {
}
if (error && Object.keys(this.observers).length > 0) {
this.observeObjectChanges();
}
//We're done receiving from the provider. No more chunks.
intermediateResponse.resolve(true);
return intermediateResponse.promise;
}
/**

View File

@@ -27,6 +27,8 @@ import {
describe('the plugin', () => {
let openmct;
let element;
let child;
let provider;
let testPath = '/test/db';
let options;
@@ -34,8 +36,6 @@ describe('the plugin', () => {
let mockDomainObject;
beforeEach((done) => {
spyOnBuiltins(['fetch'], window);
mockDomainObject = {
identifier: {
namespace: '',
@@ -51,6 +51,8 @@ describe('the plugin', () => {
};
openmct = createOpenMct(false);
spyOnBuiltins(['fetch'], window);
openmct.$injector = jasmine.createSpyObj('$injector', ['get']);
mockIdentifierService = jasmine.createSpyObj(
'identifierService',
@@ -68,6 +70,10 @@ describe('the plugin', () => {
openmct.types.addType('mock-type', {creatable: true});
element = document.createElement('div');
child = document.createElement('div');
element.appendChild(child);
openmct.on('start', done);
openmct.startHeadless();

View File

@@ -1,25 +1,3 @@
<!--
Open MCT, Copyright (c) 2014-2020, United States Government
as represented by the Administrator of the National Aeronautics and Space
Administration. All rights reserved.
Open MCT is licensed under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0.
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
License for the specific language governing permissions and limitations
under the License.
Open MCT includes source code licensed under additional open source
licenses. See the Open Source Licenses file (LICENSES.md) included with
this source code distribution or the Licensing information page available
at runtime from the About dialog for additional information.
-->
<template>
<div ref="plan"
class="c-plan c-timeline-holder"
@@ -50,6 +28,7 @@ import SwimLane from "@/ui/components/swim-lane/SwimLane.vue";
import { getValidatedPlan } from "./util";
import Vue from "vue";
//TODO: UI direction needed for the following property values
const PADDING = 1;
const OUTER_TEXT_PADDING = 12;
const INNER_TEXT_PADDING = 17;
@@ -302,9 +281,7 @@ export default {
exceeds: {
start: this.xScale(this.viewBounds.start) > this.xScale(activity.start),
end: this.xScale(this.viewBounds.end) < this.xScale(activity.end)
},
start: activity.start,
end: activity.end
}
},
textLines: textLines,
textStart: textStart,
@@ -362,9 +339,6 @@ export default {
components: {
SwimLane
},
provide: {
openmct: this.openmct
},
data() {
return {
heading,
@@ -402,6 +376,7 @@ export default {
activityRows.forEach((row) => {
const items = activitiesByRow[row];
items.forEach(item => {
//TODO: Don't draw the left-border of the rectangle if the activity started before viewBounds.start
this.plotActivity(item, parseInt(row, 10), groupSVG);
});
});
@@ -424,9 +399,6 @@ export default {
element.setAttributeNS(null, key, attributes[key]);
});
},
getNSAttributesForElement(element, attribute) {
return element.getAttributeNS(null, attribute);
},
// Experimental for now - unused
addForeignElement(svgElement, label, x, y) {
let foreign = document.createElementNS('http://www.w3.org/2000/svg', "foreignObject");
@@ -471,10 +443,6 @@ export default {
fill: activity.color
});
rectElement.addEventListener('click', (event) => {
this.setSelectionForActivity(event.currentTarget, activity, event.metaKey);
});
svgElement.appendChild(rectElement);
item.textLines.forEach((line, index) => {
@@ -488,9 +456,6 @@ export default {
const textNode = document.createTextNode(line);
textElement.appendChild(textNode);
textElement.addEventListener('click', (event) => {
this.setSelectionForActivity(event.currentTarget, activity, event.metaKey);
});
svgElement.appendChild(textElement);
});
// this.addForeignElement(svgElement, activity.name, item.textStart, item.textY - LINE_HEIGHT);
@@ -517,22 +482,6 @@ export default {
const cBrightness = ((hR * 299) + (hG * 587) + (hB * 114)) / 1000;
return cBrightness > cThreshold ? "#000000" : "#ffffff";
},
setSelectionForActivity(element, activity, multiSelect) {
this.openmct.selection.select([{
element: element,
context: {
type: 'activity',
activity: activity
}
}, {
element: this.openmct.layout.$refs.browseObject.$el,
context: {
item: this.domainObject,
supportsMultiSelect: true
}
}], multiSelect);
event.stopPropagation();
}
}
};

View File

@@ -1,206 +0,0 @@
<!--
Open MCT, Copyright (c) 2014-2021, United States Government
as represented by the Administrator of the National Aeronautics and Space
Administration. All rights reserved.
Open MCT is licensed under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0.
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
License for the specific language governing permissions and limitations
under the License.
Open MCT includes source code licensed under additional open source
licenses. See the Open Source Licenses file (LICENSES.md) included with
this source code distribution or the Licensing information page available
at runtime from the About dialog for additional information.
-->
<template>
<div class="c-inspector__properties c-inspect-properties">
<plan-activity-view v-for="activity in activities"
:key="activity.id"
:activity="activity"
:heading="heading"
/>
</div>
</template>
<script>
import PlanActivityView from "./PlanActivityView.vue";
import { getPreciseDuration } from "utils/duration";
import uuid from 'uuid';
const propertyLabels = {
'start': 'Start DateTime',
'end': 'End DateTime',
'duration': 'Duration',
'earliestStart': 'Earliest Start',
'latestEnd': 'Latest End',
'gap': 'Gap',
'overlap': 'Overlap',
'totalTime': 'Total Time'
};
export default {
components: {
PlanActivityView
},
inject: ['openmct', 'selection'],
data() {
return {
name: '',
activities: [],
heading: ''
};
},
mounted() {
this.setFormatters();
this.getPlanData(this.selection);
this.getActivities();
this.openmct.selection.on('change', this.updateSelection);
this.openmct.time.on('timeSystem', this.setFormatters);
},
beforeDestroy() {
this.openmct.selection.off('change', this.updateSelection);
this.openmct.time.off('timeSystem', this.setFormatters);
},
methods: {
setFormatters() {
let timeSystem = this.openmct.time.timeSystem();
this.timeFormatter = this.openmct.telemetry.getValueFormatter({
format: timeSystem.timeFormat
}).formatter;
},
updateSelection(newSelection) {
this.getPlanData(newSelection);
this.getActivities();
},
getPlanData(selection) {
this.selectedActivities = [];
selection.forEach((selectionItem) => {
if (selectionItem[0].context.type === 'activity') {
const activity = selectionItem[0].context.activity;
if (activity) {
this.selectedActivities.push(activity);
}
}
});
},
getActivities() {
if (this.selectedActivities.length <= 1) {
this.heading = 'Time';
this.setSingleActivityProperties();
} else {
this.heading = 'Convex Hull';
this.setMultipleActivityProperties();
}
},
setSingleActivityProperties() {
this.activities.splice(0);
this.selectedActivities.forEach((selectedActivity, index) => {
const activity = {
id: uuid(),
start: {
label: propertyLabels.start,
value: this.formatTime(selectedActivity.start)
},
end: {
label: propertyLabels.end,
value: this.formatTime(selectedActivity.end)
},
duration: {
label: propertyLabels.duration,
value: this.formatDuration(selectedActivity.end - selectedActivity.start)
}
};
this.$set(this.activities, index, activity);
});
},
sortFn(a, b) {
const numA = parseInt(a.start, 10);
const numB = parseInt(b.start, 10);
if (numA > numB) {
return 1;
}
if (numA < numB) {
return -1;
}
return 0;
},
setMultipleActivityProperties() {
this.activities.splice(0);
let earliestStart;
let latestEnd;
let gap;
let overlap;
//Sort by start time
let selectedActivities = this.selectedActivities.sort(this.sortFn);
selectedActivities.forEach((selectedActivity, index) => {
if (selectedActivities.length === 2 && index > 0) {
const previous = selectedActivities[index - 1];
//they're on different rows so there must be overlap
if (previous.end > selectedActivity.start) {
overlap = previous.end - selectedActivity.start;
} else if (previous.end < selectedActivity.start) {
gap = selectedActivity.start - previous.end;
}
}
if (index > 0) {
earliestStart = Math.min(earliestStart, selectedActivity.start);
latestEnd = Math.max(latestEnd, selectedActivity.end);
} else {
earliestStart = selectedActivity.start;
latestEnd = selectedActivity.end;
}
});
let totalTime = latestEnd - earliestStart;
const activity = {
id: uuid(),
'earliestStart': {
label: propertyLabels.earliestStart,
value: this.formatTime(earliestStart)
},
'latestEnd': {
label: propertyLabels.latestEnd,
value: this.formatTime(latestEnd)
}
};
if (gap) {
activity.gap = {
label: propertyLabels.gap,
value: this.formatDuration(gap)
};
} else if (overlap) {
activity.overlap = {
label: propertyLabels.overlap,
value: this.formatDuration(overlap)
};
}
activity.totalTime = {
label: propertyLabels.totalTime,
value: this.formatDuration(totalTime)
};
this.$set(this.activities, 0, activity);
},
formatDuration(duration) {
return getPreciseDuration(duration);
},
formatTime(time) {
return this.timeFormatter.format(time);
}
}
};
</script>

View File

@@ -1,84 +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.
-->
<template>
<div v-if="timeProperties.length"
class="u-contents"
>
<div class="c-inspect-properties__header">
{{ heading }}
</div>
<ul v-for="timeProperty in timeProperties"
:key="timeProperty.id"
class="c-inspect-properties__section"
>
<activity-property :label="timeProperty.label"
:value="timeProperty.value"
/>
</ul>
</div>
</template>
<script>
import ActivityProperty from './ActivityProperty.vue';
import uuid from 'uuid';
export default {
components: {
ActivityProperty
},
props: {
activity: {
type: Object,
required: true
},
heading: {
type: String,
required: true
}
},
data() {
return {
timeProperties: []
};
},
mounted() {
this.setProperties();
},
methods: {
setProperties() {
Object.keys(this.activity).forEach((key) => {
if (this.activity[key].label) {
const label = this.activity[key].label;
const value = String(this.activity[key].value);
this.$set(this.timeProperties, this.timeProperties.length, {
id: uuid(),
label,
value
});
}
});
}
}
};
</script>

View File

@@ -1,69 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, 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 PlanActivitiesView from "./PlanActivitiesView.vue";
import Vue from 'vue';
export default function PlanInspectorViewProvider(openmct) {
return {
key: 'plan-inspector',
name: 'Plan Inspector View',
canView: function (selection) {
if (selection.length === 0 || selection[0].length === 0) {
return false;
}
let context = selection[0][0].context;
return context
&& context.type === 'activity';
},
view: function (selection) {
let component;
return {
show: function (element) {
component = new Vue({
el: element,
components: {
PlanActivitiesView: PlanActivitiesView
},
provide: {
openmct,
selection: selection
},
template: '<plan-activities-view></plan-activities-view>'
});
},
destroy: function () {
if (component) {
component.$destroy();
component = undefined;
}
}
};
},
priority: function () {
return 1;
}
};
}

View File

@@ -1,25 +1,3 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, 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.
*****************************************************************************/
.c-plan {
svg {
text-rendering: geometricPrecision;

View File

@@ -21,7 +21,6 @@
*****************************************************************************/
import PlanViewProvider from './PlanViewProvider';
import PlanInspectorViewProvider from "./inspector/PlanInspectorViewProvider";
export default function () {
return function install(openmct) {
@@ -38,14 +37,16 @@ export default function () {
control: 'file-input',
required: true,
text: 'Select File...',
type: 'application/json'
type: 'application/json',
property: [
"selectFile"
]
}
],
initialize: function (domainObject) {
}
});
openmct.objectViews.addProvider(new PlanViewProvider(openmct));
openmct.inspectorViews.addProvider(new PlanInspectorViewProvider(openmct));
};
}

View File

@@ -29,10 +29,9 @@ describe('the plugin', function () {
let element;
let child;
let openmct;
let appHolder;
beforeEach((done) => {
appHolder = document.createElement('div');
const appHolder = document.createElement('div');
appHolder.style.width = '640px';
appHolder.style.height = '480px';
@@ -104,7 +103,7 @@ describe('the plugin', function () {
];
let planView;
beforeEach(() => {
beforeEach((done) => {
planDomainObject = {
identifier: {
key: 'test-object',
@@ -141,7 +140,9 @@ describe('the plugin', function () {
let view = planView.view(planDomainObject, mockObjectPath);
view.show(child, true);
return Vue.nextTick();
return Vue.nextTick().then(() => {
done();
});
});
it('loads activities into the view', () => {

Some files were not shown because too many files have changed in this diff Show More