topic-form-refactor (#4478)

* Form refactor (#3816)
* New form API and associated form controls
* Actions updated to use new form API.

Co-authored-by: Jamie V <jamie.j.vigliotta@nasa.gov>
Co-authored-by: charlesh88 <charles.f.hacskaylo@nasa.gov>

* Reimplementation of import export json (#4171)

Co-authored-by: Jamie V <jamie.j.vigliotta@nasa.gov>
Co-authored-by: charlesh88 <charles.f.hacskaylo@nasa.gov>
Co-authored-by: Henry Hsu <hhsu0219@gmail.com>
Co-authored-by: Andrew Henry <akhenry@gmail.com>
This commit is contained in:
Nikhil
2021-12-07 12:27:23 -08:00
committed by GitHub
parent e20c7a17d6
commit 8acbcadd5d
126 changed files with 3275 additions and 6954 deletions

View File

@ -1,190 +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>
<li class="c-tree__item-h">
<div
class="c-tree__item"
:class="{ 'is-alias': isAlias, 'is-navigated-object': navigated }"
@click="handleItemSelected(node.object, node)"
>
<view-control
v-model="expanded"
class="c-tree__item__view-control"
:enabled="hasChildren"
/>
<div class="c-tree__item__label c-object-label">
<div
class="c-tree__item__type-icon c-object-label__type-icon"
:class="typeClass"
></div>
<div class="c-tree__item__name c-object-label__name">{{ node.object.name }}</div>
</div>
</div>
<ul
v-if="expanded && !isLoading"
class="c-tree"
>
<li
v-if="isLoading && !loaded"
class="c-tree__item-h"
>
<div class="c-tree__item loading">
<span class="c-tree__item__label">Loading...</span>
</div>
</li>
<condition-set-dialog-tree-item
v-for="child in children"
:key="child.id"
:node="child"
:selected-item="selectedItem"
:handle-item-selected="handleItemSelected"
/>
</ul>
</li>
</template>
<script>
import viewControl from '@/ui/components/viewControl.vue';
export default {
name: 'ConditionSetDialogTreeItem',
components: {
viewControl
},
inject: ['openmct'],
props: {
node: {
type: Object,
required: true
},
selectedItem: {
type: Object,
default() {
return undefined;
}
},
handleItemSelected: {
type: Function,
default() {
return (item) => {};
}
}
},
data() {
return {
hasChildren: false,
isLoading: false,
loaded: false,
children: [],
expanded: false
};
},
computed: {
navigated() {
const itemId = this.selectedItem && this.selectedItem.itemId;
const isSelectedObject = itemId && this.openmct.objects.areIdsEqual(this.node.object.identifier, itemId);
if (isSelectedObject && this.node.objectPath && this.node.objectPath.length > 1) {
const isParent = this.openmct.objects.areIdsEqual(this.node.objectPath[1].identifier, this.selectedItem.parentId);
return isSelectedObject && isParent;
}
return isSelectedObject;
},
isAlias() {
let parent = this.node.objectPath[1];
if (!parent) {
return false;
}
let parentKeyString = this.openmct.objects.makeKeyString(parent.identifier);
return parentKeyString !== this.node.object.location;
},
typeClass() {
let type = this.openmct.types.get(this.node.object.type);
if (!type) {
return 'icon-object-unknown';
}
return type.definition.cssClass;
}
},
watch: {
expanded() {
if (!this.hasChildren) {
return;
}
if (!this.loaded && !this.isLoading) {
this.composition = this.openmct.composition.get(this.domainObject);
this.composition.on('add', this.addChild);
this.composition.on('remove', this.removeChild);
this.composition.load().then(this.finishLoading);
this.isLoading = true;
}
}
},
mounted() {
this.domainObject = this.node.object;
let removeListener = this.openmct.objects.observe(this.domainObject, '*', (newObject) => {
this.domainObject = newObject;
});
this.$once('hook:destroyed', removeListener);
if (this.openmct.composition.get(this.node.object)) {
this.hasChildren = true;
}
},
beforeDestroy() {
this.expanded = false;
},
destroyed() {
if (this.composition) {
this.composition.off('add', this.addChild);
this.composition.off('remove', this.removeChild);
delete this.composition;
}
},
methods: {
addChild(child) {
this.children.push({
id: this.openmct.objects.makeKeyString(child.identifier),
object: child,
objectPath: [child].concat(this.node.objectPath),
navigateToParent: this.navigateToPath
});
},
removeChild(identifier) {
let removeId = this.openmct.objects.makeKeyString(identifier);
this.children = this.children
.filter(c => c.id !== removeId);
},
finishLoading() {
this.isLoading = false;
this.loaded = true;
}
}
};
</script>

View File

@ -1,192 +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="u-contents">
<div 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-tree-and-search__search">
<search ref="shell-search"
class="c-search"
:value="searchValue"
@input="searchTree"
@clear="searchTree"
/>
</div>
<!-- loading -->
<div v-if="isLoading"
class="c-tree-and-search__loading loading"
></div>
<!-- end loading -->
<div v-if="shouldDisplayNoResultsText"
class="c-tree-and-search__no-results"
>
No results found
</div>
<!-- main tree -->
<ul v-if="!isLoading"
v-show="!searchValue"
class="c-tree-and-search__tree c-tree"
>
<condition-set-dialog-tree-item
v-for="treeItem in allTreeItems"
:key="treeItem.id"
:node="treeItem"
:selected-item="selectedItem"
:handle-item-selected="handleItemSelection"
/>
</ul>
<!-- end main tree -->
<!-- search tree -->
<ul v-if="searchValue && !isLoading"
class="c-tree-and-search__tree c-tree"
>
<condition-set-dialog-tree-item
v-for="treeItem in filteredTreeItems"
:key="treeItem.id"
:node="treeItem"
:selected-item="selectedItem"
:handle-item-selected="handleItemSelection"
/>
</ul>
<!-- end search tree -->
</div>
</div>
</template>
<script>
import debounce from 'lodash/debounce';
import search from '@/ui/components/search.vue';
import ConditionSetDialogTreeItem from './ConditionSetDialogTreeItem.vue';
export default {
name: 'ConditionSetSelectorDialog',
components: {
search,
ConditionSetDialogTreeItem
},
inject: ['openmct'],
data() {
return {
expanded: false,
searchValue: '',
allTreeItems: [],
filteredTreeItems: [],
isLoading: false,
selectedItem: undefined
};
},
computed: {
shouldDisplayNoResultsText() {
if (this.isLoading) {
return false;
}
return this.allTreeItems.length === 0
|| (this.searchValue && this.filteredTreeItems.length === 0);
}
},
created() {
this.getDebouncedFilteredChildren = debounce(this.getFilteredChildren, 400);
},
mounted() {
this.getAllChildren();
},
methods: {
getAllChildren() {
this.isLoading = true;
this.openmct.objects.get('ROOT')
.then(root => {
return this.openmct.composition.get(root).load();
})
.then(children => {
this.isLoading = false;
this.allTreeItems = children.map(c => {
return {
id: this.openmct.objects.makeKeyString(c.identifier),
object: c,
objectPath: [c],
navigateToParent: '/browse'
};
});
});
},
getFilteredChildren() {
// clear any previous search results
this.filteredTreeItems = [];
const promises = this.openmct.objects.search(this.searchValue)
.map(promise => promise
.then(results => this.aggregateFilteredChildren(results)));
Promise.all(promises).then(() => {
this.isLoading = false;
});
},
async aggregateFilteredChildren(results) {
for (const object of results) {
const objectPath = await this.openmct.objects.getOriginalPath(object.identifier);
const navigateToParent = '/browse/'
+ objectPath.slice(1)
.map(parent => this.openmct.objects.makeKeyString(parent.identifier))
.join('/');
const filteredChild = {
id: this.openmct.objects.makeKeyString(object.identifier),
object,
objectPath,
navigateToParent
};
this.filteredTreeItems.push(filteredChild);
}
},
searchTree(value) {
this.searchValue = value;
this.isLoading = true;
if (this.searchValue !== '') {
this.getDebouncedFilteredChildren();
} else {
this.isLoading = false;
}
},
handleItemSelection(item, node) {
if (item && item.type === 'conditionSet') {
const parentId = (node.objectPath && node.objectPath.length > 1) ? node.objectPath[1].identifier : undefined;
this.selectedItem = {
itemId: item.identifier,
parentId
};
this.$emit('conditionSetSelected', item);
}
}
}
};
</script>

View File

@ -101,12 +101,12 @@
<script>
import StyleEditor from "./StyleEditor.vue";
import ConditionSetSelectorDialog from "./ConditionSetSelectorDialog.vue";
import SelectorDialogTree from '@/ui/components/SelectorDialogTree.vue';
import ConditionDescription from "@/plugins/condition/components/ConditionDescription.vue";
import ConditionError from "@/plugins/condition/components/ConditionError.vue";
import Vue from 'vue';
import PreviewAction from "@/ui/preview/PreviewAction.js";
import {getApplicableStylesForItem} from "@/plugins/condition/utils/styleUtils";
import { getApplicableStylesForItem } from "@/plugins/condition/utils/styleUtils";
import isEmpty from 'lodash/isEmpty';
export default {
@ -224,7 +224,7 @@ export default {
let conditionSetDomainObject;
let self = this;
function handleItemSelection(item) {
function handleItemSelection({ item }) {
if (item) {
conditionSetDomainObject = item;
}
@ -240,16 +240,17 @@ export default {
}
let vm = new Vue({
components: {ConditionSetSelectorDialog},
components: { SelectorDialogTree },
provide: {
openmct: this.openmct
},
data() {
return {
handleItemSelection
handleItemSelection,
title: 'Select Condition Set'
};
},
template: '<condition-set-selector-dialog @conditionSetSelected="handleItemSelection"></condition-set-selector-dialog>'
template: '<selector-dialog-tree :title="title" @treeItemSelected="handleItemSelection"></selector-dialog-tree>'
}).$mount();
let overlay = this.openmct.overlays.overlay({

View File

@ -127,7 +127,7 @@ import FontStyleEditor from '@/ui/inspector/styles/FontStyleEditor.vue';
import StyleEditor from "./StyleEditor.vue";
import PreviewAction from "@/ui/preview/PreviewAction.js";
import { getApplicableStylesForItem, getConsolidatedStyleValues, getConditionSetIdentifierForItem } from "@/plugins/condition/utils/styleUtils";
import ConditionSetSelectorDialog from "@/plugins/condition/components/inspector/ConditionSetSelectorDialog.vue";
import SelectorDialogTree from '@/ui/components/SelectorDialogTree.vue';
import ConditionError from "@/plugins/condition/components/ConditionError.vue";
import ConditionDescription from "@/plugins/condition/components/ConditionDescription.vue";
import Vue from 'vue';
@ -539,7 +539,7 @@ export default {
addConditionSet() {
let conditionSetDomainObject;
let self = this;
function handleItemSelection(item) {
function handleItemSelection({ item }) {
if (item) {
conditionSetDomainObject = item;
}
@ -556,16 +556,17 @@ export default {
}
let vm = new Vue({
components: {ConditionSetSelectorDialog},
components: { SelectorDialogTree },
provide: {
openmct: this.openmct
},
data() {
return {
handleItemSelection
handleItemSelection,
title: 'Select Condition Set'
};
},
template: '<condition-set-selector-dialog @conditionSetSelected="handleItemSelection"></condition-set-selector-dialog>'
template: '<SelectorDialogTree :title="title" @treeItemSelected="handleItemSelection"></SelectorDialogTree>'
}).$mount();
let overlay = this.openmct.overlays.overlay({

View File

@ -42,7 +42,7 @@ define(['lodash'], function (_) {
toolbar: function (selectedObjects) {
const DIALOG_FORM = {
'text': {
name: "Text Element Properties",
title: "Text Element Properties",
sections: [
{
rows: [
@ -50,14 +50,15 @@ define(['lodash'], function (_) {
key: "text",
control: "textfield",
name: "Text",
required: true
required: true,
cssClass: "l-input-lg"
}
]
}
]
},
'image': {
name: "Image Properties",
title: "Image Properties",
sections: [
{
rows: [
@ -65,7 +66,7 @@ define(['lodash'], function (_) {
key: "url",
control: "textfield",
name: "Image URL",
"cssClass": "l-input-lg",
cssClass: "l-input-lg",
required: true
}
]
@ -126,10 +127,6 @@ define(['lodash'], function (_) {
]
};
function getUserInput(form) {
return openmct.$injector.get('dialogService').getUserInput(form, {});
}
function getPath(selectionPath) {
return `configuration.items[${selectionPath[0].context.index}]`;
}
@ -167,8 +164,7 @@ define(['lodash'], function (_) {
let name = option.name.toLowerCase();
let form = DIALOG_FORM[name];
if (form) {
getUserInput(form)
.then(element => selectionPath[0].context.addElement(name, element));
showForm(form, name, selectionPath);
} else {
selectionPath[0].context.addElement(name);
}
@ -643,10 +639,18 @@ define(['lodash'], function (_) {
&& !selectionPath[0].context.layoutItem;
}
function showForm(formStructure, name, selectionPath) {
openmct.forms.showForm(formStructure)
.then(changes => {
selectionPath[0].context.addElement(name, changes);
});
}
if (isMainLayoutSelected(selectedObjects[0])) {
return [
getToggleGridButton(selectedObjects),
getAddButton(selectedObjects)];
getAddButton(selectedObjects)
];
}
let toolbar = {

View File

@ -33,96 +33,78 @@ export default class DuplicateAction {
this.openmct = openmct;
}
async invoke(objectPath) {
let duplicationTask = new DuplicateTask(this.openmct);
let originalObject = objectPath[0];
let parent = objectPath[1];
let userInput;
invoke(objectPath) {
this.object = objectPath[0];
this.parent = objectPath[1];
try {
userInput = await this.getUserInput(originalObject, parent);
} catch (error) {
// user most likely canceled
return;
}
this.showForm(this.object, this.parent);
}
let newParent = userInput.location;
let inNavigationPath = this.inNavigationPath(originalObject);
inNavigationPath() {
return this.openmct.router.path
.some(objectInPath => this.openmct.objects.areIdsEqual(objectInPath.identifier, this.object.identifier));
}
// legacy check
if (this.isLegacyDomainObject(newParent)) {
newParent = await this.convertFromLegacy(newParent);
}
// if editing, save
onSave(changes) {
let inNavigationPath = this.inNavigationPath();
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);
let duplicationTask = new DuplicateTask(this.openmct);
if (changes.name && (changes.name !== this.object.name)) {
duplicationTask.changeName(changes.name);
}
const parentDomainObjectpath = changes.location || [this.parent];
const parent = parentDomainObjectpath[0];
return duplicationTask.duplicate(this.object, parent);
}
inNavigationPath(object) {
return this.openmct.router.path
.some(objectInPath => this.openmct.objects.areIdsEqual(objectInPath.identifier, object.identifier));
}
getDialogForm(object, parent) {
return {
name: "Duplicate Item",
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",
control: "locator",
validate: this.validate(object, parent),
required: true,
parent: parentDomainObject,
validate: this.validate(parentDomainObject),
key: 'location'
}
]
}
]
};
this.openmct.forms.showForm(formStructure)
.then(this.onSave.bind(this));
}
validate(object, currentParent) {
return (parentCandidate) => {
let currentParentKeystring = this.openmct.objects.makeKeyString(currentParent.identifier);
let parentCandidateKeystring = this.openmct.objects.makeKeyString(parentCandidate.getId());
let objectKeystring = this.openmct.objects.makeKeyString(object.identifier);
validate(currentParent) {
return (data) => {
const parentCandidatePath = data.value;
const parentCandidate = parentCandidatePath[0];
if (!parentCandidate || !currentParentKeystring) {
let currentParentKeystring = this.openmct.objects.makeKeyString(currentParent.identifier);
let parentCandidateKeystring = this.openmct.objects.makeKeyString(parentCandidate.identifier);
let objectKeystring = this.openmct.objects.makeKeyString(this.object.identifier);
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, this.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;
@ -43,10 +42,15 @@ export default class DuplicateTask {
this.persisted = 0;
this.clones = [];
this.idMap = {};
this.name = undefined;
this.openmct = openmct;
}
changeName(name) {
this.name = name;
}
/**
* Execute the duplicate/copy task with the objects provided.
* @returns {promise} Which will resolve with a clone of the object
@ -79,11 +83,14 @@ export default class DuplicateTask {
*/
async buildDuplicationPlan() {
let domainObjectClone = await this.duplicateObject(this.domainObject);
if (domainObjectClone !== this.domainObject) {
domainObjectClone.location = this.getKeyString(this.parent);
}
if (this.name) {
domainObjectClone.name = this.name;
}
this.firstClone = domainObjectClone;
return;

View File

@ -20,7 +20,6 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
import DuplicateActionPlugin from './plugin.js';
import DuplicateAction from './DuplicateAction.js';
import DuplicateTask from './DuplicateTask.js';
import {
createOpenMct,
@ -29,7 +28,6 @@ import {
} from 'utils/testing';
describe("The Duplicate Action plugin", () => {
let openmct;
let duplicateTask;
let childObject;
@ -52,6 +50,7 @@ describe("The Duplicate Action plugin", () => {
}
}
}).folder;
parentObject = getMockObjects({
objectKeyStrings: ['folder'],
overwrite: {
@ -62,6 +61,7 @@ describe("The Duplicate Action plugin", () => {
}
}
}).folder;
anotherParentObject = getMockObjects({
objectKeyStrings: ['folder'],
overwrite: {
@ -120,7 +120,6 @@ describe("The Duplicate Action plugin", () => {
});
describe("when moving an object to a new parent", () => {
beforeEach(async () => {
duplicateTask = new DuplicateTask(openmct);
await duplicateTask.duplicate(parentObject, anotherParentObject);
@ -142,16 +141,14 @@ describe("The Duplicate Action plugin", () => {
});
describe("when a new name is provided for the duplicated object", () => {
it("the name is updated", () => {
it("the name is updated", async () => {
const NEW_NAME = 'New Name';
let childName;
duplicateTask = new DuplicateAction(openmct);
duplicateTask.updateNameCheck(parentObject, NEW_NAME);
duplicateTask = new DuplicateTask(openmct);
duplicateTask.changeName(NEW_NAME);
const child = await duplicateTask.duplicate(childObject, anotherParentObject);
childName = parentObject.name;
expect(childName).toEqual(NEW_NAME);
expect(child.name).toEqual(NEW_NAME);
});
});

View File

@ -0,0 +1,202 @@
/*****************************************************************************
* 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 JSONExporter from '/src/exporters/JSONExporter.js';
import _ from 'lodash';
import { saveAs } from 'saveAs';
import uuid from "uuid";
export default class ExportAsJSONAction {
constructor(openmct) {
this.openmct = openmct;
this.name = 'Export as JSON';
this.key = 'export.JSON';
this.description = '';
this.cssClass = "icon-export";
this.group = "json";
this.priority = 1;
this.externalIdentifiers = [];
this.tree = {};
this.calls = 0;
this.idMap = {};
this.JSONExportService = new JSONExporter(saveAs);
}
// Public
/**
*
* @param {object} objectPath
* @returns {boolean}
*/
appliesTo(objectPath) {
let domainObject = objectPath[0];
return this._isCreatable(domainObject);
}
/**
*
* @param {object} objectpath
*/
invoke(objectpath) {
const root = objectpath[0];
this.root = JSON.parse(JSON.stringify(root));
const rootId = this._getId(this.root);
this.tree[rootId] = this.root;
this._write(this.root);
}
/**
* @private
* @param {object} domainObject
* @returns {string} A string representation of the given identifier, including namespace and key
*/
_getId(domainObject) {
return this.openmct.objects.makeKeyString(domainObject.identifier);
}
/**
* @private
* @param {object} domainObject
* @returns {boolean}
*/
_isCreatable(domainObject) {
const type = this.openmct.types.get(domainObject.type);
return type && type.definition.creatable;
}
/**
* @private
* @param {object} child
* @param {object} parent
* @returns {boolean}
*/
_isLinkedObject(child, parent) {
if (child.location !== this._getId(parent)
&& !Object.keys(this.tree).includes(child.location)
&& this._getId(child) !== this._getId(this.root)
|| this.externalIdentifiers.includes(this._getId(child))) {
return true;
}
return false;
}
/**
* @private
* @param {object} child
* @param {object} parent
* @returns {object}
*/
_rewriteLink(child, parent) {
this.externalIdentifiers.push(this._getId(child));
const index = parent.composition.findIndex(id => {
return _.isEqual(child.identifier, id);
});
const copyOfChild = JSON.parse(JSON.stringify(child));
copyOfChild.identifier.key = uuid();
const newIdString = this._getId(copyOfChild);
const parentId = this._getId(parent);
this.idMap[this._getId(child)] = newIdString;
copyOfChild.location = parentId;
parent.composition[index] = copyOfChild.identifier;
this.tree[newIdString] = copyOfChild;
this.tree[parentId].composition[index] = copyOfChild.identifier;
return copyOfChild;
}
/**
* @private
*/
_rewriteReferences() {
let treeString = JSON.stringify(this.tree);
Object.keys(this.idMap).forEach(function (oldId) {
const newId = this.idMap[oldId];
treeString = treeString.split(oldId).join(newId);
}.bind(this));
this.tree = JSON.parse(treeString);
}
/**
* @private
* @param {object} completedTree
*/
_saveAs(completedTree) {
this.JSONExportService.export(
completedTree,
{ filename: this.root.name + '.json' }
);
}
/**
* @private
* @returns {object}
*/
_wrapTree() {
return {
"openmct": this.tree,
"rootId": this._getId(this.root)
};
}
/**
* @private
* @param {object} parent
*/
_write(parent) {
this.calls++;
const composition = this.openmct.composition.get(parent);
if (composition !== undefined) {
composition.load()
.then((children) => {
children.forEach((child, index) => {
// Only export if object is creatable
if (this._isCreatable(child)) {
// Prevents infinite export of self-contained objs
if (!Object.prototype.hasOwnProperty.call(this.tree, this._getId(child))) {
// If object is a link to something absent from
// tree, generate new id and treat as new object
if (this._isLinkedObject(child, parent)) {
child = this._rewriteLink(child, parent);
} else {
this.tree[this._getId(child)] = child;
}
this._write(child);
}
}
});
this.calls--;
if (this.calls === 0) {
this._rewriteReferences();
this._saveAs(this._wrapTree());
}
});
} else {
this.calls--;
if (this.calls === 0) {
this._rewriteReferences();
this._saveAs(this._wrapTree());
}
}
}
}

View File

@ -0,0 +1,252 @@
/*****************************************************************************
* 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/ExportAsJSONAction",
"../../../entanglement/test/DomainObjectFactory",
"../../../../src/MCT",
'../../../../src/adapter/capabilities/AdapterCapability'
],
function (ExportAsJSONAction, domainObjectFactory, MCT, AdapterCapability) {
describe("The export JSON action", function () {
let context;
let action;
let exportService;
let identifierService;
let typeService;
let openmct;
let policyService;
let mockType;
let mockObjectProvider;
let exportedTree;
beforeEach(function () {
openmct = new MCT();
mockObjectProvider = {
objects: {},
get: function (id) {
return Promise.resolve(mockObjectProvider.objects[id.key]);
}
};
openmct.objects.addProvider('', mockObjectProvider);
exportService = jasmine.createSpyObj('exportService',
['exportJSON']);
identifierService = jasmine.createSpyObj('identifierService',
['generate']);
policyService = jasmine.createSpyObj('policyService',
['allow']);
mockType = jasmine.createSpyObj('type', ['hasFeature']);
typeService = jasmine.createSpyObj('typeService', [
'getType'
]);
mockType.hasFeature.and.callFake(function (feature) {
return feature === 'creation';
});
typeService.getType.and.returnValue(mockType);
context = {};
context.domainObject = domainObjectFactory(
{
name: 'test',
id: 'someID',
capabilities: {
'adapter': {
invoke: invokeAdapter
}
}
});
identifierService.generate.and.returnValue('brandNewId');
exportService.exportJSON.and.callFake(function (tree, options) {
exportedTree = tree;
});
policyService.allow.and.callFake(function (capability, type) {
return type.hasFeature(capability);
});
action = new ExportAsJSONAction(openmct, exportService, policyService,
identifierService, typeService, context);
});
function invokeAdapter() {
let newStyleObject = new AdapterCapability(context.domainObject).invoke();
return newStyleObject;
}
it("initializes happily", function () {
expect(action).toBeDefined();
});
xit("doesn't export non-creatable objects in tree", function () {
let nonCreatableType = {
hasFeature:
function (feature) {
return feature !== 'creation';
}
};
typeService.getType.and.returnValue(nonCreatableType);
let parent = domainObjectFactory({
name: 'parent',
model: {
name: 'parent',
location: 'ROOT',
composition: ['childId']
},
id: 'parentId',
capabilities: {
'adapter': {
invoke: invokeAdapter
}
}
});
let child = {
identifier: {
namespace: '',
key: 'childId'
},
name: 'child',
location: 'parentId'
};
context.domainObject = parent;
addChild(child);
action.perform();
return new Promise(function (resolve, reject) {
setTimeout(resolve, 100);
}).then(function () {
expect(Object.keys(action.tree).length).toBe(1);
expect(Object.prototype.hasOwnProperty.call(action.tree, "parentId"))
.toBeTruthy();
});
});
xit("can export self-containing objects", function () {
let parent = domainObjectFactory({
name: 'parent',
model: {
name: 'parent',
location: 'ROOT',
composition: ['infiniteChildId']
},
id: 'infiniteParentId',
capabilities: {
'adapter': {
invoke: invokeAdapter
}
}
});
let child = {
identifier: {
namespace: '',
key: 'infiniteChildId'
},
name: 'child',
location: 'infiniteParentId',
composition: ['infiniteParentId']
};
addChild(child);
context.domainObject = parent;
action.perform();
return new Promise(function (resolve, reject) {
setTimeout(resolve, 100);
}).then(function () {
expect(Object.keys(action.tree).length).toBe(2);
expect(Object.prototype.hasOwnProperty.call(action.tree, "infiniteParentId"))
.toBeTruthy();
expect(Object.prototype.hasOwnProperty.call(action.tree, "infiniteChildId"))
.toBeTruthy();
});
});
xit("exports links to external objects as new objects", function () {
let parent = domainObjectFactory({
name: 'parent',
model: {
name: 'parent',
composition: ['externalId'],
location: 'ROOT'
},
id: 'parentId',
capabilities: {
'adapter': {
invoke: invokeAdapter
}
}
});
let externalObject = {
name: 'external',
location: 'outsideOfTree',
identifier: {
namespace: '',
key: 'externalId'
}
};
addChild(externalObject);
context.domainObject = parent;
return new Promise (function (resolve) {
action.perform();
setTimeout(resolve, 100);
}).then(function () {
expect(Object.keys(action.tree).length).toBe(2);
expect(Object.prototype.hasOwnProperty.call(action.tree, "parentId"))
.toBeTruthy();
expect(Object.prototype.hasOwnProperty.call(action.tree, "brandNewId"))
.toBeTruthy();
expect(action.tree.brandNewId.location).toBe('parentId');
});
});
it("exports object tree in the correct format", function () {
action.perform();
return new Promise(function (resolve, reject) {
setTimeout(resolve, 100);
}).then(function () {
expect(Object.keys(exportedTree).length).toBe(2);
expect(Object.prototype.hasOwnProperty.call(exportedTree, "openmct")).toBeTruthy();
expect(Object.prototype.hasOwnProperty.call(exportedTree, "rootId")).toBeTruthy();
});
});
function addChild(object) {
mockObjectProvider.objects[object.identifier.key] = object;
}
});
}
);

View File

@ -0,0 +1,28 @@
/*****************************************************************************
* 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 ExportAsJSONAction from './ExportAsJSONAction';
export default function () {
return function (openmct) {
openmct.actions.register(new ExportAsJSONAction(openmct));
};
}

View File

@ -0,0 +1,150 @@
/*****************************************************************************
* 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
*/
async _onSave(changes) {
let parentDomainObjectPath;
Object.entries(changes).forEach(([key, value]) => {
if (key === 'location') {
parentDomainObjectPath = value;
return;
}
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;
});
const parentDomainObject = parentDomainObjectPath[0];
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 this.openmct.composition.get(parentDomainObject);
compositionCollection.add(this.domainObject);
this._navigateAndEdit(this.domainObject, parentDomainObjectPath);
this.openmct.notifications.info('Save successful');
} else {
this.openmct.notifications.error('Error saving objects');
}
dialog.dismiss();
}
/**
* @private
*/
async _navigateAndEdit(domainObject, parentDomainObjectpath) {
let objectPath;
if (parentDomainObjectpath) {
objectPath = parentDomainObjectpath && [domainObject].concat(parentDomainObjectpath);
} else {
objectPath = await this.openmct.objects.getOriginalPath(domainObject.identifier);
}
const url = '#/browse/' + objectPath
.map(object => object && this.openmct.objects.makeKeyString(object.identifier.key))
.reverse()
.join('/');
this.openmct.router.navigate(url);
const objectView = this.openmct.objectViews.get(domainObject, objectPath)[0];
const canEdit = objectView && objectView.canEdit && objectView.canEdit(domainObject, objectPath);
if (canEdit) {
this.openmct.editor.edit();
}
}
/**
* @private
*/
_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;
this.openmct.forms.showForm(formStructure)
.then(this._onSave.bind(this));
}
}

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-input-lg',
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(data) {
return self.openmct.composition.checkPolicy(data.value[0], domainObject);
}
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];
}
}
}

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
*/
async _onSave(changes) {
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');
}
dialog.dismiss();
}
/**
* @private
*/
_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;
this.openmct.forms.showForm(formStructure)
.then(this._onSave.bind(this));
}
}

View File

@ -0,0 +1,35 @@
/*****************************************************************************
* 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 PropertiesAction {
constructor(openmct) {
this.openmct = openmct;
}
/**
* @private
*/
_getTypeDefinition(type) {
const TypeDefinition = this.openmct.types.get(type);
return TypeDefinition.definition;
}
}

View File

@ -0,0 +1,29 @@
/*****************************************************************************
* 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 './EditPropertiesAction';
export default function () {
return function (openmct) {
openmct.actions.register(new EditPropertiesAction(openmct));
};
}

View File

@ -0,0 +1,249 @@
/*****************************************************************************
* 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 objectUtils from 'objectUtils';
import uuid from "uuid";
export default class ImportAsJSONAction {
constructor(openmct) {
this.name = 'Import from JSON';
this.key = 'import.JSON';
this.description = '';
this.cssClass = "icon-import";
this.group = "json";
this.priority = 2;
this.openmct = openmct;
}
// Public
/**
*
* @param {object} objectPath
* @returns {boolean}
*/
appliesTo(objectPath) {
const domainObject = objectPath[0];
if (domainObject && domainObject.locked) {
return false;
}
return domainObject !== undefined
&& this.openmct.composition.get(domainObject);
}
/**
*
* @param {object} objectPath
*/
invoke(objectPath) {
this._showForm(objectPath[0]);
}
/**
*
* @param {object} object
* @param {object} changes
*/
onSave(object, changes) {
const selectFile = changes.selectFile;
const objectTree = selectFile.body;
this._importObjectTree(object, JSON.parse(objectTree));
}
/**
* @private
* @param {object} parent
* @param {object} tree
* @param {object} seen
*/
_deepInstantiate(parent, tree, seen) {
if (this.openmct.composition.get(parent)) {
let newObj;
seen.push(parent.id);
parent.composition.forEach(async (childId) => {
const keystring = this.openmct.objects.makeKeyString(childId);
if (!tree[keystring] || seen.includes(keystring)) {
return;
}
const newModel = tree[keystring];
delete newModel.persisted;
newObj = await this._instantiate(newModel);
this._deepInstantiate(newObj, tree, seen);
}, this);
}
}
/**
* @private
* @param {object} tree
* @param {string} namespace
* @returns {object}
*/
_generateNewIdentifiers(tree, namespace) {
// For each domain object in the file, generate new ID, replace in tree
Object.keys(tree.openmct).forEach(domainObjectId => {
const newId = {
namespace,
key: uuid()
};
const oldId = objectUtils.parseKeyString(domainObjectId);
tree = this._rewriteId(oldId, newId, tree);
}, this);
return tree;
}
/**
* @private
* @param {object} domainObject
* @param {object} objTree
*/
async _importObjectTree(domainObject, objTree) {
const namespace = domainObject.identifier.namespace;
const tree = this._generateNewIdentifiers(objTree, namespace);
const rootId = tree.rootId;
const rootModel = tree.openmct[rootId];
delete rootModel.persisted;
const rootObj = await this._instantiate(rootModel);
if (this.openmct.composition.checkPolicy(domainObject, rootObj)) {
this._deepInstantiate(rootObj, tree.openmct, []);
const compositionCollection = this.openmct.composition.get(domainObject);
let domainObjectKeyString = this.openmct.objects.makeKeyString(domainObject.identifier);
this.openmct.objects.mutate(rootObj, 'location', domainObjectKeyString);
compositionCollection.add(rootObj);
} else {
const dialog = this.openmct.overlays.dialog({
iconClass: 'alert',
message: "We're sorry, but you cannot import that object type into this object.",
buttons: [
{
label: "Ok",
emphasis: true,
callback: function () {
dialog.dismiss();
}
}
]
});
}
}
/**
* @private
* @param {object} rootModel
* @returns {object}
*/
async _instantiate(rootModel) {
const success = await this.openmct.objects.save(rootModel);
if (success) {
return rootModel;
}
this.openmct.notifications.error('Error saving objects');
}
/**
* @private
* @param {object} oldId
* @param {object} newId
* @param {object} tree
* @returns {object}
*/
_rewriteId(oldId, newId, tree) {
let newIdKeyString = this.openmct.objects.makeKeyString(newId);
let oldIdKeyString = this.openmct.objects.makeKeyString(oldId);
tree = JSON.stringify(tree).replace(new RegExp(oldIdKeyString, 'g'), newIdKeyString);
return JSON.parse(tree, (key, value) => {
if (value !== undefined
&& value !== null
&& Object.prototype.hasOwnProperty.call(value, 'key')
&& Object.prototype.hasOwnProperty.call(value, 'namespace')
&& value.key === oldId.key
&& value.namespace === oldId.namespace) {
return newId;
} else {
return value;
}
});
}
/**
* @private
* @param {object} domainObject
*/
_showForm(domainObject) {
const formStructure = {
title: this.name,
sections: [
{
rows: [
{
name: 'Select File',
key: 'selectFile',
control: 'file-input',
required: true,
text: 'Select File...',
validate: this._validateJSON,
type: 'application/json'
}
]
}
]
};
this.openmct.forms.showForm(formStructure).
then(changes => {
let onSave = this.onSave.bind(this);
onSave(domainObject, changes);
});
}
/**
* @private
* @param {object} data
* @returns {boolean}
*/
_validateJSON(data) {
const value = data.value;
const objectTree = value && value.body;
let json;
let success = true;
try {
json = JSON.parse(objectTree);
} catch (e) {
success = false;
}
if (success && (!json.openmct || !json.rootId)) {
success = false;
}
if (!success) {
this.openmct.notifications.error('Invalid File: The selected file was either invalid JSON or was not formatted properly for import into Open MCT.');
}
return success;
}
}

View File

@ -0,0 +1,249 @@
/*****************************************************************************
* 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/ImportAsJSONAction",
"../../../entanglement/test/DomainObjectFactory"
],
function (ImportAsJSONAction, domainObjectFactory) {
describe("The import JSON action", function () {
let context = {};
let action;
let exportService;
let identifierService;
let dialogService;
let openmct;
let mockDialog;
let compositionCapability;
let mockInstantiate;
let uniqueId;
let newObjects;
beforeEach(function () {
uniqueId = 0;
newObjects = [];
openmct = {
$injector: jasmine.createSpyObj('$injector', ['get']),
objects: {
makeKeyString: function (identifier) {
return identifier.key;
},
isPersistable: jasmine.createSpy('isPersistable')
}
};
openmct.objects.isPersistable.and.returnValue(true);
mockInstantiate = jasmine.createSpy('instantiate').and.callFake(
function (model, id) {
let config = {
"model": model,
"id": id,
"capabilities": {}
};
let locationCapability = {
setPrimaryLocation: jasmine.createSpy('setPrimaryLocation')
.and
.callFake(function (newLocation) {
config.model.location = newLocation;
})
};
config.capabilities.location = locationCapability;
if (model.composition) {
let compCapability =
jasmine.createSpy('compCapability')
.and.returnValue(model.composition);
compCapability.add = jasmine.createSpy('add')
.and.callFake(function (newObj) {
config.model.composition.push(newObj.getId());
});
config.capabilities.composition = compCapability;
}
newObjects.push(domainObjectFactory(config));
return domainObjectFactory(config);
});
openmct.$injector.get.and.returnValue(mockInstantiate);
dialogService = jasmine.createSpyObj('dialogService',
[
'getUserInput',
'showBlockingMessage'
]
);
identifierService = jasmine.createSpyObj('identifierService',
[
'generate'
]
);
identifierService.generate.and.callFake(function () {
uniqueId++;
return uniqueId;
});
compositionCapability = jasmine.createSpy('compositionCapability');
mockDialog = jasmine.createSpyObj("dialog", ["dismiss"]);
dialogService.showBlockingMessage.and.returnValue(mockDialog);
action = new ImportAsJSONAction(exportService, identifierService,
dialogService, openmct, context);
});
it("initializes happily", function () {
expect(action).toBeDefined();
});
it("only applies to objects with composition capability", function () {
let compDomainObject = domainObjectFactory({
name: 'compObject',
model: { name: 'compObject'},
capabilities: {"composition": compositionCapability}
});
let noCompDomainObject = domainObjectFactory();
context.domainObject = compDomainObject;
expect(ImportAsJSONAction.appliesTo(context, undefined, openmct)).toBe(true);
context.domainObject = noCompDomainObject;
expect(ImportAsJSONAction.appliesTo(context, undefined, openmct)).toBe(false);
});
it("checks object persistability", function () {
const compDomainObject = domainObjectFactory({
name: 'compObject',
model: { name: 'compObject'},
capabilities: {"composition": compositionCapability}
});
context.domainObject = compDomainObject;
ImportAsJSONAction.appliesTo(context, undefined, openmct);
expect(openmct.objects.isPersistable).toHaveBeenCalled();
});
it("displays error dialog on invalid file choice", function () {
dialogService.getUserInput.and.returnValue(Promise.resolve(
{
selectFile: {
body: JSON.stringify({badKey: "INVALID"}),
name: "fileName"
}
})
);
action.perform();
return new Promise(function (resolve, reject) {
setTimeout(resolve, 100);
}).then(function () {
expect(dialogService.getUserInput).toHaveBeenCalled();
expect(dialogService.showBlockingMessage).toHaveBeenCalled();
});
});
xit("can import self-containing objects", function () {
let compDomainObject = domainObjectFactory({
name: 'compObject',
model: { name: 'compObject'},
capabilities: {"composition": compositionCapability}
});
context.domainObject = compDomainObject;
dialogService.getUserInput.and.returnValue(Promise.resolve(
{
selectFile: {
body: JSON.stringify({
"openmct": {
"infiniteParent": {
"composition": [{
key: "infinteChild",
namespace: ""
}],
"name": "1",
"type": "folder",
"modified": 1503598129176,
"location": "mine",
"persisted": 1503598129176
},
"infinteChild": {
"composition": [{
key: "infinteParent",
namespace: ""
}],
"name": "2",
"type": "folder",
"modified": 1503598132428,
"location": "infiniteParent",
"persisted": 1503598132428
}
},
"rootId": "infiniteParent"
}),
name: "fileName"
}
})
);
action.perform();
return new Promise(function (resolve, reject) {
setTimeout(resolve, 100);
}).then(function () {
expect(mockInstantiate.calls.count()).toEqual(2);
});
});
xit("assigns new ids to each imported object", function () {
dialogService.getUserInput.and.returnValue(Promise.resolve(
{
selectFile: {
body: JSON.stringify({
"openmct": {
"cce9f107-5060-4f55-8151-a00120f4222f": {
"composition": [],
"name": "test",
"type": "folder",
"modified": 1503596596639,
"location": "mine",
"persisted": 1503596596639
}
},
"rootId": "cce9f107-5060-4f55-8151-a00120f4222f"
}),
name: "fileName"
}
})
);
action.perform();
return new Promise(function (resolve, reject) {
setTimeout(resolve, 100);
}).then(function () {
expect(mockInstantiate.calls.count()).toEqual(1);
expect(newObjects[0].getId()).toBe('1');
});
});
});
}
);

View File

@ -0,0 +1,28 @@
/*****************************************************************************
* 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 ImportFromJSONAction from './ImportFromJSONAction';
export default function () {
return function (openmct) {
openmct.actions.register(new ImportFromJSONAction(openmct));
};
}

View File

@ -0,0 +1,120 @@
/*****************************************************************************
* 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;
}
appliesTo(objectPath) {
let domainObject = objectPath[0];
let type = domainObject && this.openmct.types.get(domainObject.type);
return type && type.definition.creatable;
}
invoke(objectPath) {
this.object = objectPath[0];
this.parent = objectPath[1];
this.showForm(this.object, this.parent);
}
inNavigationPath() {
return this.openmct.router.path
.some(objectInPath => this.openmct.objects.areIdsEqual(objectInPath.identifier, this.object.identifier));
}
onSave(changes) {
let inNavigationPath = this.inNavigationPath();
if (inNavigationPath && this.openmct.editor.isEditing()) {
this.openmct.editor.save();
}
const parentDomainObjectpath = changes.location || [this.parent];
const parent = parentDomainObjectpath[0];
this.linkInNewParent(this.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)
.then(this.onSave.bind(this));
}
validate(currentParent) {
return (object, data) => {
const parentCandidate = data.value;
const currentParentKeystring = this.openmct.objects.makeKeyString(currentParent.identifier);
const parentCandidateKeystring = this.openmct.objects.makeKeyString(parentCandidate.identifier);
const 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);
};
}
}

View File

@ -0,0 +1,28 @@
/*****************************************************************************
* 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 LinkAction from "./LinkAction";
export default function () {
return function (openmct) {
openmct.actions.register(new LinkAction(openmct));
};
}

View File

@ -0,0 +1,125 @@
/*****************************************************************************
* 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';
const LINK_ACITON_NAME = 'Create 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", () => {
const actionCollection = openmct.actions.getActionsCollection([childObject]);
const visibleActions = actionCollection.getVisibleActions();
linkAction = visibleActions.find(a => a.key === LINK_ACITON_KEY);
expect(linkAction.name).toEqual(LINK_ACITON_NAME);
});
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

@ -31,53 +31,16 @@ export default class MoveAction {
this.openmct = openmct;
}
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;
invoke(objectPath) {
this.object = objectPath[0];
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(this.object, this.oldParent);
}
inNavigationPath(object) {
inNavigationPath() {
return this.openmct.router.path
.some(objectInPath => this.openmct.objects.areIdsEqual(objectInPath.identifier, object.identifier));
.some(objectInPath => this.openmct.objects.areIdsEqual(objectInPath.identifier, this.object.identifier));
}
navigateTo(objectPath) {
@ -85,7 +48,7 @@ export default class MoveAction {
.map(object => this.openmct.objects.makeKeyString(object.identifier))
.join("/");
window.location.href = '#/browse/' + urlPath;
this.openmct.router.navigate('#/browse/' + urlPath);
}
addToNewParent(child, newParent) {
@ -96,42 +59,91 @@ export default class MoveAction {
compositionCollection.add(child);
}
removeFromOldParent(parent, child) {
let compositionCollection = this.openmct.composition.get(parent);
async onSave(changes) {
let inNavigationPath = this.inNavigationPath(this.object);
if (inNavigationPath && this.openmct.editor.isEditing()) {
this.openmct.editor.save();
}
const parentDomainObjectpath = changes.location || [this.parent];
const parent = parentDomainObjectpath[0];
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 !== this.object.name)) {
this.object.name = changes.name;
}
this.addToNewParent(this.object, parent);
this.removeFromOldParent(this.object);
if (!inNavigationPath) {
return;
}
let newObjectPath;
if (parentDomainObjectpath) {
newObjectPath = parentDomainObjectpath && [this.object].concat(parentDomainObjectpath);
} else {
newObjectPath = await this.openmct.objects.getOriginalPath(this.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",
control: "locator",
validate: this.validate(object, parent),
required: true,
validate: this.validate(parentDomainObject),
key: 'location'
}
]
}
]
};
this.openmct.forms.showForm(formStructure)
.then(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 (!parentCandidateKeystring || !currentParentKeystring) {
@ -146,14 +158,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

@ -26,7 +26,6 @@ import {
} from 'utils/testing';
describe("The Move Action plugin", () => {
let openmct;
let moveAction;
let childObject;
@ -49,20 +48,30 @@ describe("The Move Action plugin", () => {
}
}
}).folder;
parentObject = getMockObjects({
objectKeyStrings: ['folder'],
overwrite: {
folder: {
name: "Parent Folder",
composition: [childObject.identifier]
composition: [childObject.identifier],
identifier: {
namespace: "",
key: "parent-folder-object"
}
}
}
}).folder;
anotherParentObject = getMockObjects({
objectKeyStrings: ['folder'],
overwrite: {
folder: {
name: "Another Parent Folder"
name: "Another Parent Folder",
identifier: {
namespace: "",
key: "another-parent-folder-object"
}
}
}
}).folder;
@ -82,10 +91,26 @@ describe("The Move Action plugin", () => {
});
describe("when moving an object to a new parent and removing from the old parent", () => {
beforeEach((done) => {
openmct.router.path = [];
beforeEach(() => {
moveAction.addToNewParent(childObject, anotherParentObject);
moveAction.removeFromOldParent(parentObject, childObject);
spyOn(openmct.objects, "save");
openmct.objects.save.and.callThrough();
spyOn(openmct.forms, "showForm");
openmct.forms.showForm.and.callFake(formStructure => {
return Promise.resolve({
name: 'test',
location: [anotherParentObject]
});
});
openmct.objects.observe(parentObject, '*', (newObject) => {
done();
});
moveAction.inNavigationPath = () => false;
moveAction.invoke([childObject, parentObject]);
});
it("the child object's identifier should be in the new parent's composition", () => {
@ -98,5 +123,4 @@ describe("The Move Action plugin", () => {
expect(oldParentComposition.length).toEqual(0);
});
});
});

View File

@ -19,11 +19,11 @@
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import uuid from 'uuid';
import CreateAction from '@/plugins/formActions/CreateAction';
export default class NewFolderAction {
constructor(openmct) {
this.type = 'folder';
this.name = 'Add New Folder';
this.key = 'newFolder';
this.description = 'Create a new folder';
@ -32,59 +32,18 @@ export default class NewFolderAction {
this.priority = 9;
this._openmct = openmct;
this._dialogForm = {
name: "Add New Folder",
sections: [
{
rows: [
{
key: "name",
control: "textfield",
name: "Folder Name",
pattern: "\\S+",
required: true,
cssClass: "l-input-lg"
}
]
}
]
};
}
invoke(objectPath) {
let domainObject = objectPath[0];
let parentKeystring = this._openmct.objects.makeKeyString(domainObject.identifier);
let composition = this._openmct.composition.get(domainObject);
let dialogService = this._openmct.$injector.get('dialogService');
let folderType = this._openmct.types.get('folder');
dialogService.getUserInput(this._dialogForm, {name: 'Unnamed Folder'}).then((userInput) => {
let name = userInput.name;
let identifier = {
key: uuid(),
namespace: domainObject.identifier.namespace
};
let objectModel = {
identifier,
type: 'folder',
location: parentKeystring
};
folderType.definition.initialize(objectModel);
objectModel.name = name || 'New Folder';
objectModel.modified = Date.now();
this._openmct.objects.save(objectModel).then(() => {
composition.add(objectModel);
});
});
const parentDomainObject = objectPath[0];
const createAction = new CreateAction(this._openmct, this.type, parentDomainObject);
createAction.invoke();
}
appliesTo(objectPath) {
let domainObject = objectPath[0];
let isPersistable = this._openmct.objects.isPersistable(domainObject.identifier);
return domainObject.type === 'folder' && isPersistable;
return domainObject.type === this.type && isPersistable;
}
}

View File

@ -26,13 +26,7 @@ import {
describe("the plugin", () => {
let openmct;
let compositionAPI;
let newFolderAction;
let mockObjectPath;
let mockDialogService;
let mockComposition;
let mockPromise;
let newFolderName = 'New Folder';
beforeEach((done) => {
openmct = createOpenMct();
@ -52,39 +46,40 @@ describe("the plugin", () => {
});
describe('when invoked', () => {
let parentObject;
let parentObjectPath;
let changedParentObject;
beforeEach((done) => {
compositionAPI = openmct.composition;
mockObjectPath = [{
parentObject = {
name: 'mock folder',
type: 'folder',
identifier: {
key: 'mock-folder',
namespace: ''
}
}];
mockPromise = {
then: (callback) => {
callback({name: newFolderName});
done();
}
},
composition: []
};
parentObjectPath = [parentObject];
mockDialogService = jasmine.createSpyObj('dialogService', ['getUserInput']);
mockComposition = jasmine.createSpyObj('composition', ['add']);
spyOn(openmct.objects, "save");
openmct.objects.save.and.callThrough();
mockDialogService.getUserInput.and.returnValue(mockPromise);
spyOn(openmct.forms, "showForm");
openmct.forms.showForm.and.callFake(formStructure => {
return Promise.resolve({
name: 'test',
notes: 'test notes',
location: parentObjectPath
});
});
spyOn(openmct.$injector, 'get').and.returnValue(mockDialogService);
spyOn(compositionAPI, 'get').and.returnValue(mockComposition);
spyOn(openmct.objects, 'save').and.returnValue(Promise.resolve(true));
spyOn(openmct.objects, 'isPersistable').and.returnValue(true);
openmct.objects.observe(parentObject, '*', (newObject) => {
changedParentObject = newObject;
return newFolderAction.invoke(mockObjectPath);
});
done();
});
it('gets user input for folder name', () => {
expect(mockDialogService.getUserInput).toHaveBeenCalled();
newFolderAction.invoke(parentObjectPath);
});
it('creates a new folder object', () => {
@ -92,10 +87,23 @@ describe("the plugin", () => {
});
it('adds new folder object to parent composition', () => {
expect(mockComposition.add).toHaveBeenCalled();
const composition = changedParentObject.composition;
expect(composition.length).toBe(1);
});
it('checks if the domainObject is persistable', () => {
const mockObjectPath = [{
name: 'mock folder',
type: 'folder',
identifier: {
key: 'mock-folder',
namespace: ''
}
}];
spyOn(openmct.objects, 'isPersistable').and.returnValue(true);
newFolderAction.appliesTo(mockObjectPath);
expect(openmct.objects.isPersistable).toHaveBeenCalled();

View File

@ -71,6 +71,7 @@ export default function NotebookPlugin() {
name: 'Section Title',
control: 'textfield',
cssClass: 'l-inline',
required: true,
property: [
"configuration",
"sectionTitle"
@ -81,6 +82,7 @@ export default function NotebookPlugin() {
name: 'Page Title',
control: 'textfield',
cssClass: 'l-inline',
required: true,
property: [
"configuration",
"pageTitle"

View File

@ -38,7 +38,10 @@ export default function (configuration) {
control: 'file-input',
required: true,
text: 'Select File...',
type: 'application/json'
type: 'application/json',
property: [
"selectFile"
]
}
],
initialize: function (domainObject) {

View File

@ -32,7 +32,6 @@ define([
'./timeConductor/plugin',
'../../example/imagery/plugin',
'./imagery/plugin',
'../../platform/import-export/bundle',
'./summaryWidget/plugin',
'./URLIndicatorPlugin/URLIndicatorPlugin',
'./telemetryMean/plugin',
@ -42,6 +41,7 @@ define([
'./staticRootPlugin/plugin',
'./notebook/plugin',
'./displayLayout/plugin',
'./formActions/plugin',
'./folderView/plugin',
'./flexibleLayout/plugin',
'./tabs/plugin',
@ -87,7 +87,6 @@ define([
TimeConductorPlugin,
ExampleImagery,
ImageryPlugin,
ImportExport,
SummaryWidget,
URLIndicatorPlugin,
TelemetryMean,
@ -97,6 +96,7 @@ define([
StaticRootPlugin,
Notebook,
DisplayLayoutPlugin,
FormActions,
FolderView,
FlexibleLayout,
Tabs,
@ -150,8 +150,6 @@ define([
plugins.MyItems = MyItems.default;
plugins.ImportExport = ImportExport;
plugins.StaticRootPlugin = StaticRootPlugin;
/**
@ -206,6 +204,7 @@ define([
plugins.URLIndicator = URLIndicatorPlugin;
plugins.Notebook = Notebook.default;
plugins.DisplayLayout = DisplayLayoutPlugin.default;
plugins.FormActions = FormActions;
plugins.FolderView = FolderView;
plugins.Tabs = Tabs;
plugins.FlexibleLayout = FlexibleLayout;

View File

@ -85,10 +85,6 @@ describe("the plugin", () => {
viewDatumAction.invoke(mockObjectPath, mockView);
done();
});
it('uses an overlay to show user datum values', () => {
expect(openmct.overlays.overlay).toHaveBeenCalled();
});
});