Compare commits
	
		
			9 Commits
		
	
	
		
			mutation-p
			...
			release/1.
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | a2301e67d1 | ||
|   | 44cbd3fc50 | ||
|   | 858de567ae | ||
|   | ad22661029 | ||
|   | 26aa49d9a8 | ||
|   | cb55500cba | ||
|   | 340246ab26 | ||
|   | 9cb743fb48 | ||
|   | 9c52ec7233 | 
| @@ -82,6 +82,7 @@ | ||||
|         ); | ||||
|  | ||||
|         openmct.install(openmct.plugins.LocalStorage()); | ||||
|  | ||||
|         openmct.install(openmct.plugins.Espresso()); | ||||
|         openmct.install(openmct.plugins.MyItems()); | ||||
|         openmct.install(openmct.plugins.Generator()); | ||||
|   | ||||
| @@ -102,7 +102,7 @@ module.exports = (config) => { | ||||
|         }, | ||||
|         specReporter: { | ||||
|             maxLogLines: 5, | ||||
|             suppressErrorSummary: true, | ||||
|             suppressErrorSummary: false, | ||||
|             suppressFailed: false, | ||||
|             suppressPassed: false, | ||||
|             suppressSkipped: true, | ||||
|   | ||||
| @@ -71,13 +71,15 @@ define(['zepto', 'objectUtils'], function ($, objectUtils) { | ||||
|         var rootObj = this.instantiate(rootModel, rootId); | ||||
|         var newStyleParent = parent.useCapability('adapter'); | ||||
|         var newStyleRootObj = rootObj.useCapability('adapter'); | ||||
|         newStyleRootObj.location = parent.getId(); | ||||
|  | ||||
|         if (this.openmct.composition.checkPolicy(newStyleParent, newStyleRootObj)) { | ||||
|             // Instantiate all objects in tree with their newly generated ids, | ||||
|             // adding each to its rightful parent's composition | ||||
|             rootObj.getCapability("location").setPrimaryLocation(parent.getId()); | ||||
|             this.deepInstantiate(rootObj, tree.openmct, []); | ||||
|             parent.getCapability("composition").add(rootObj); | ||||
|             this.openmct.objects.save(newStyleRootObj); | ||||
|             const compositionCollection = this.openmct.composition.get(newStyleParent); | ||||
|             compositionCollection.add(newStyleRootObj); | ||||
|         } else { | ||||
|             var dialog = this.openmct.overlays.dialog({ | ||||
|                 iconClass: 'alert', | ||||
| @@ -103,7 +105,6 @@ define(['zepto', 'objectUtils'], function ($, objectUtils) { | ||||
|             var newObj; | ||||
|  | ||||
|             seen.push(parent.getId()); | ||||
|  | ||||
|             parentModel.composition.forEach(function (childId) { | ||||
|                 let keystring = this.openmct.objects.makeKeyString(childId); | ||||
|  | ||||
| @@ -115,8 +116,7 @@ define(['zepto', 'objectUtils'], function ($, objectUtils) { | ||||
|                 delete newModel.persisted; | ||||
|  | ||||
|                 newObj = this.instantiate(newModel, keystring); | ||||
|                 newObj.getCapability("location") | ||||
|                     .setPrimaryLocation(tree[keystring].location); | ||||
|                 this.openmct.objects.save(newModel); | ||||
|                 this.deepInstantiate(newObj, tree, seen); | ||||
|             }, this); | ||||
|         } | ||||
|   | ||||
| @@ -42,15 +42,21 @@ define( | ||||
|                 newObjects; | ||||
|  | ||||
|             beforeEach(function () { | ||||
|  | ||||
|                 uniqueId = 0; | ||||
|                 newObjects = []; | ||||
|                 openmct = { | ||||
|                     $injector: jasmine.createSpyObj('$injector', ['get']), | ||||
|                     objects: { | ||||
|                         makeKeyString: function (identifier) { | ||||
|                             return identifier.key; | ||||
|                         } | ||||
|                         makeKeyString: identifier => identifier.key, | ||||
|                         save: o => true | ||||
|                     }, | ||||
|                     composition: { | ||||
|                         get: (o) => { | ||||
|                             return { | ||||
|                                 add: v => {} | ||||
|                             }; | ||||
|                         }, | ||||
|                         checkPolicy: (a, b) => true | ||||
|                     } | ||||
|                 }; | ||||
|                 mockInstantiate = jasmine.createSpy('instantiate').and.callFake( | ||||
| @@ -60,14 +66,6 @@ define( | ||||
|                             "id": id, | ||||
|                             "capabilities": {} | ||||
|                         }; | ||||
|                         var locationCapability = { | ||||
|                             setPrimaryLocation: jasmine.createSpy('setPrimaryLocation') | ||||
|                                 .and | ||||
|                                 .callFake(function (newLocation) { | ||||
|                                     config.model.location = newLocation; | ||||
|                                 }) | ||||
|                         }; | ||||
|                         config.capabilities.location = locationCapability; | ||||
|                         if (model.composition) { | ||||
|                             var compCapability = | ||||
|                                 jasmine.createSpy('compCapability') | ||||
| @@ -79,6 +77,10 @@ define( | ||||
|                             config.capabilities.composition = compCapability; | ||||
|                         } | ||||
|  | ||||
|                         config.capabilities.adapter = { | ||||
|                             invoke: () => model | ||||
|                         }; | ||||
|  | ||||
|                         newObjects.push(domainObjectFactory(config)); | ||||
|  | ||||
|                         return domainObjectFactory(config); | ||||
| @@ -146,14 +148,33 @@ define( | ||||
|                 }); | ||||
|             }); | ||||
|  | ||||
|             xit("can import self-containing objects", function () { | ||||
|             it("can import self-containing objects", function () { | ||||
|                 var compDomainObject = domainObjectFactory({ | ||||
|                     name: 'compObject', | ||||
|                     model: { name: 'compObject'}, | ||||
|                     capabilities: {"composition": compositionCapability} | ||||
|                     capabilities: { | ||||
|                         "composition": compositionCapability, | ||||
|                         'adapter': { | ||||
|                             invoke: () => { | ||||
|                                 return { | ||||
|                                     name: 'parent', | ||||
|                                     composition: [], | ||||
|                                     id: "mine", | ||||
|                                     identifier: { | ||||
|                                         namespace: '', | ||||
|                                         key: 'mine' | ||||
|                                     }, | ||||
|                                     location: "ROOT", | ||||
|                                     modified: 1637287323760, | ||||
|                                     persisted: 1637287323760, | ||||
|                                     type: "folder" | ||||
|                                 }; | ||||
|                             } | ||||
|                         } | ||||
|                     } | ||||
|                 }); | ||||
|                 context.domainObject = compDomainObject; | ||||
|  | ||||
|                 context.domainObject = compDomainObject; | ||||
|                 dialogService.getUserInput.and.returnValue(Promise.resolve( | ||||
|                     { | ||||
|                         selectFile: { | ||||
| @@ -161,10 +182,14 @@ define( | ||||
|                                 "openmct": { | ||||
|                                     "infiniteParent": { | ||||
|                                         "composition": [{ | ||||
|                                             key: "infinteChild", | ||||
|                                             namespace: "" | ||||
|                                             "key": "infinteChild", | ||||
|                                             "namespace": "" | ||||
|                                         }], | ||||
|                                         "name": "1", | ||||
|                                         "identifier": { | ||||
|                                             "key": "infiniteParent", | ||||
|                                             "namespace": "" | ||||
|                                         }, | ||||
|                                         "name": "parent", | ||||
|                                         "type": "folder", | ||||
|                                         "modified": 1503598129176, | ||||
|                                         "location": "mine", | ||||
| @@ -172,10 +197,14 @@ define( | ||||
|                                     }, | ||||
|                                     "infinteChild": { | ||||
|                                         "composition": [{ | ||||
|                                             key: "infinteParent", | ||||
|                                             namespace: "" | ||||
|                                             "key": "infiniteParent", | ||||
|                                             "namespace": "" | ||||
|                                         }], | ||||
|                                         "name": "2", | ||||
|                                         "identifier": { | ||||
|                                             "key": "infinteChild", | ||||
|                                             "namespace": "" | ||||
|                                         }, | ||||
|                                         "name": "child", | ||||
|                                         "type": "folder", | ||||
|                                         "modified": 1503598132428, | ||||
|                                         "location": "infiniteParent", | ||||
| @@ -198,7 +227,7 @@ define( | ||||
|                 }); | ||||
|             }); | ||||
|  | ||||
|             xit("assigns new ids to each imported object", function () { | ||||
|             it("assigns new ids to each imported object", function () { | ||||
|                 dialogService.getUserInput.and.returnValue(Promise.resolve( | ||||
|                     { | ||||
|                         selectFile: { | ||||
| @@ -229,7 +258,6 @@ define( | ||||
|                     expect(newObjects[0].getId()).toBe('1'); | ||||
|                 }); | ||||
|             }); | ||||
|  | ||||
|         }); | ||||
|     } | ||||
| ); | ||||
|   | ||||
| @@ -184,6 +184,15 @@ ObjectAPI.prototype.get = function (identifier, abortSignal) { | ||||
|     } | ||||
|  | ||||
|     identifier = utils.parseKeyString(identifier); | ||||
|     let dirtyObject; | ||||
|     if (this.isTransactionActive()) { | ||||
|         dirtyObject = this.transaction.getDirtyObject(keystring); | ||||
|     } | ||||
|  | ||||
|     if (dirtyObject) { | ||||
|         return Promise.resolve(dirtyObject); | ||||
|     } | ||||
|  | ||||
|     const provider = this.getProvider(identifier); | ||||
|  | ||||
|     if (!provider) { | ||||
|   | ||||
| @@ -55,6 +55,17 @@ export default class Transaction { | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     getDirtyObject(keystring) { | ||||
|         let dirtyObject; | ||||
|         this.dirtyObjects.forEach(object => { | ||||
|             if (this.objectAPI.makeKeyString(object.identifier) === keystring) { | ||||
|                 dirtyObject = object; | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|         return dirtyObject; | ||||
|     } | ||||
|  | ||||
|     start() { | ||||
|         this.dirtyObjects = new Set(); | ||||
|     } | ||||
|   | ||||
| @@ -48,6 +48,7 @@ const CONTEXT_MENU_ACTIONS = [ | ||||
|     'viewHistoricalData', | ||||
|     'remove' | ||||
| ]; | ||||
| const BLANK_VALUE = '---'; | ||||
|  | ||||
| export default { | ||||
|     inject: ['openmct', 'currentView'], | ||||
| @@ -67,15 +68,43 @@ export default { | ||||
|     }, | ||||
|     data() { | ||||
|         return { | ||||
|             datum: undefined, | ||||
|             timestamp: undefined, | ||||
|             value: '---', | ||||
|             valueClass: '', | ||||
|             timestampKey: undefined, | ||||
|             unit: '' | ||||
|         }; | ||||
|     }, | ||||
|     computed: { | ||||
|         value() { | ||||
|             if (!this.datum) { | ||||
|                 return BLANK_VALUE; | ||||
|             } | ||||
|  | ||||
|             return this.formats[this.valueKey].format(this.datum); | ||||
|         }, | ||||
|         valueClass() { | ||||
|             if (!this.datum) { | ||||
|                 return ''; | ||||
|             } | ||||
|  | ||||
|             const limit = this.limitEvaluator.evaluate(this.datum, this.valueMetadata); | ||||
|  | ||||
|             return limit ? limit.cssClass : ''; | ||||
|  | ||||
|         }, | ||||
|         formattedTimestamp() { | ||||
|             return this.timestamp !== undefined ? this.getFormattedTimestamp(this.timestamp) : '---'; | ||||
|             if (!this.timestamp) { | ||||
|                 return BLANK_VALUE; | ||||
|             } | ||||
|  | ||||
|             return this.timeSystemFormat.format(this.timestamp); | ||||
|         }, | ||||
|         timeSystemFormat() { | ||||
|             if (!this.formats[this.timestampKey]) { | ||||
|                 console.warn(`No formatter for ${this.timestampKey} time system for ${this.domainObject.name}.`); | ||||
|             } | ||||
|  | ||||
|             return this.formats[this.timestampKey]; | ||||
|         }, | ||||
|         objectPath() { | ||||
|             return [this.domainObject, ...this.pathToTable]; | ||||
| @@ -96,15 +125,19 @@ export default { | ||||
|  | ||||
|         this.timestampKey = this.openmct.time.timeSystem().key; | ||||
|  | ||||
|         this.valueMetadata = this.metadata ? this | ||||
|             .metadata | ||||
|             .valuesForHints(['range'])[0] : undefined; | ||||
|         this.valueMetadata = undefined; | ||||
|  | ||||
|         if (this.metadata) { | ||||
|             this.valueMetadata = this | ||||
|                 .metadata | ||||
|                 .valuesForHints(['range'])[0] || this.firstNonDomainAttribute(this.metadata); | ||||
|         } | ||||
|  | ||||
|         this.valueKey = this.valueMetadata ? this.valueMetadata.key : undefined; | ||||
|  | ||||
|         this.unsubscribe = this.openmct | ||||
|             .telemetry | ||||
|             .subscribe(this.domainObject, this.updateValues); | ||||
|             .subscribe(this.domainObject, this.setLatestValues); | ||||
|  | ||||
|         this.requestHistory(); | ||||
|  | ||||
| @@ -118,29 +151,29 @@ export default { | ||||
|         this.openmct.time.off('bounds', this.updateBounds); | ||||
|     }, | ||||
|     methods: { | ||||
|         updateValues(datum) { | ||||
|             let newTimestamp = this.getParsedTimestamp(datum); | ||||
|             let limit; | ||||
|         updateView() { | ||||
|             if (!this.updatingView) { | ||||
|                 this.updatingView = true; | ||||
|                 requestAnimationFrame(() => { | ||||
|                     let newTimestamp = this.getParsedTimestamp(this.latestDatum); | ||||
|  | ||||
|             if (this.shouldUpdate(newTimestamp)) { | ||||
|                 this.datum = datum; | ||||
|                 this.timestamp = newTimestamp; | ||||
|                 this.value = this.formats[this.valueKey].format(datum); | ||||
|                 limit = this.limitEvaluator.evaluate(datum, this.valueMetadata); | ||||
|                 if (limit) { | ||||
|                     this.valueClass = limit.cssClass; | ||||
|                 } else { | ||||
|                     this.valueClass = ''; | ||||
|                 } | ||||
|                     if (this.shouldUpdate(newTimestamp)) { | ||||
|                         this.timestamp = newTimestamp; | ||||
|                         this.datum = this.latestDatum; | ||||
|                     } | ||||
|  | ||||
|                     this.updatingView = false; | ||||
|                 }); | ||||
|             } | ||||
|         }, | ||||
|         shouldUpdate(newTimestamp) { | ||||
|             let newTimestampInBounds = this.inBounds(newTimestamp); | ||||
|             let noExistingTimestamp = this.timestamp === undefined; | ||||
|             let newTimestampIsLatest = newTimestamp > this.timestamp; | ||||
|         setLatestValues(datum) { | ||||
|             this.latestDatum = datum; | ||||
|  | ||||
|             return newTimestampInBounds | ||||
|                 && (noExistingTimestamp || newTimestampIsLatest); | ||||
|             this.updateView(); | ||||
|         }, | ||||
|         shouldUpdate(newTimestamp) { | ||||
|             return this.inBounds(newTimestamp) | ||||
|                 && (this.timestamp === undefined || newTimestamp > this.timestamp); | ||||
|         }, | ||||
|         requestHistory() { | ||||
|             this.openmct | ||||
| @@ -151,7 +184,7 @@ export default { | ||||
|                     size: 1, | ||||
|                     strategy: 'latest' | ||||
|                 }) | ||||
|                 .then((array) => this.updateValues(array[array.length - 1])) | ||||
|                 .then((array) => this.setLatestValues(array[array.length - 1])) | ||||
|                 .catch((error) => { | ||||
|                     console.warn('Error fetching data', error); | ||||
|                 }); | ||||
| @@ -189,31 +222,21 @@ export default { | ||||
|             } | ||||
|         }, | ||||
|         resetValues() { | ||||
|             this.value = '---'; | ||||
|             this.timestamp = undefined; | ||||
|             this.valueClass = ''; | ||||
|             this.datum = undefined; | ||||
|         }, | ||||
|         getParsedTimestamp(timestamp) { | ||||
|             if (this.timeSystemFormat()) { | ||||
|                 return this.formats[this.timestampKey].parse(timestamp); | ||||
|             } | ||||
|         }, | ||||
|         getFormattedTimestamp(timestamp) { | ||||
|             if (this.timeSystemFormat()) { | ||||
|                 return this.formats[this.timestampKey].format(timestamp); | ||||
|             } | ||||
|         }, | ||||
|         timeSystemFormat() { | ||||
|             if (this.formats[this.timestampKey]) { | ||||
|                 return true; | ||||
|             } else { | ||||
|                 console.warn(`No formatter for ${this.timestampKey} time system for ${this.domainObject.name}.`); | ||||
|  | ||||
|                 return false; | ||||
|             if (this.timeSystemFormat) { | ||||
|                 return this.timeSystemFormat.parse(timestamp); | ||||
|             } | ||||
|         }, | ||||
|         setUnit() { | ||||
|             this.unit = this.valueMetadata.unit || ''; | ||||
|         }, | ||||
|         firstNonDomainAttribute(metadata) { | ||||
|             return metadata | ||||
|                 .values() | ||||
|                 .find(metadatum => metadatum.hints.domain === undefined && metadatum.key !== 'name'); | ||||
|         } | ||||
|     } | ||||
| }; | ||||
|   | ||||
| @@ -26,6 +26,7 @@ import { | ||||
|     getMockObjects, | ||||
|     getMockTelemetry, | ||||
|     getLatestTelemetry, | ||||
|     spyOnBuiltins, | ||||
|     resetApplicationState | ||||
| } from 'utils/testing'; | ||||
|  | ||||
| @@ -160,6 +161,11 @@ describe("The LAD Table", () => { | ||||
|                 anotherTelemetryObjectResolve = resolve; | ||||
|             }); | ||||
|  | ||||
|             spyOnBuiltins(['requestAnimationFrame']); | ||||
|             window.requestAnimationFrame.and.callFake((callBack) => { | ||||
|                 callBack(); | ||||
|             }); | ||||
|  | ||||
|             openmct.telemetry.request.and.callFake(() => { | ||||
|                 telemetryRequestResolve(mockTelemetry); | ||||
|  | ||||
|   | ||||
| @@ -263,7 +263,8 @@ export default { | ||||
|             this.openmct.telemetry.request(this.domainObject, options) | ||||
|                 .then(data => { | ||||
|                     if (data.length > 0) { | ||||
|                         this.updateView(data[data.length - 1]); | ||||
|                         this.latestDatum = data[data.length - 1]; | ||||
|                         this.updateView(); | ||||
|                     } | ||||
|                 }); | ||||
|         }, | ||||
| @@ -275,12 +276,19 @@ export default { | ||||
|                     || (datumTimeStamp | ||||
|                         && (this.openmct.time.bounds().end >= datumTimeStamp)) | ||||
|                 ) { | ||||
|                     this.updateView(datum); | ||||
|                     this.latestDatum = datum; | ||||
|                     this.updateView(); | ||||
|                 } | ||||
|             }.bind(this)); | ||||
|         }, | ||||
|         updateView(datum) { | ||||
|             this.datum = datum; | ||||
|         updateView() { | ||||
|             if (!this.updatingView) { | ||||
|                 this.updatingView = true; | ||||
|                 requestAnimationFrame(() => { | ||||
|                     this.datum = this.latestDatum; | ||||
|                     this.updatingView = false; | ||||
|                 }); | ||||
|             } | ||||
|         }, | ||||
|         removeSubscription() { | ||||
|             if (this.subscription) { | ||||
| @@ -290,7 +298,8 @@ export default { | ||||
|         }, | ||||
|         refreshData(bounds, isTick) { | ||||
|             if (!isTick) { | ||||
|                 this.datum = undefined; | ||||
|                 this.latestDatum = undefined; | ||||
|                 this.updateView(); | ||||
|                 this.requestHistoricalData(this.domainObject); | ||||
|             } | ||||
|         }, | ||||
|   | ||||
| @@ -414,7 +414,7 @@ export default { | ||||
|             if (this.indexForFocusedImage !== undefined) { | ||||
|                 imageIndex = this.initFocusedImageIndex; | ||||
|             } else { | ||||
|                 imageIndex = newSize - 1; | ||||
|                 imageIndex = newSize > 0 ? newSize - 1 : undefined; | ||||
|             } | ||||
|  | ||||
|             this.setFocusedImage(imageIndex, false); | ||||
| @@ -510,6 +510,12 @@ export default { | ||||
|                 this.timeContext.off("timeContext", this.setTimeContext); | ||||
|             } | ||||
|         }, | ||||
|         boundsChange(bounds, isTick) { | ||||
|             if (!isTick) { | ||||
|                 this.previousFocusedImage = this.focusedImage ? JSON.parse(JSON.stringify(this.focusedImage)) : undefined; | ||||
|                 this.requestHistory(); | ||||
|             } | ||||
|         }, | ||||
|         expand() { | ||||
|             const actionCollection = this.openmct.actions.getActionsCollection(this.objectPath, this.currentView); | ||||
|             const visibleActions = actionCollection.getVisibleActions(); | ||||
| @@ -670,23 +676,47 @@ export default { | ||||
|                 this.$refs.thumbsWrapper.scrollLeft = scrollWidth; | ||||
|             }); | ||||
|         }, | ||||
|         matchIndexOfPreviousImage(previous, imageHistory) { | ||||
|             // match logic uses a composite of url and time to account | ||||
|             // for example imagery not having fully unique urls | ||||
|             return imageHistory.findIndex((x) => ( | ||||
|                 x.url === previous.url | ||||
|                 && x.time === previous.time | ||||
|             )); | ||||
|         }, | ||||
|         setFocusedImage(index, thumbnailClick = false) { | ||||
|             let focusedIndex = index; | ||||
|             if (!(Number.isInteger(index) && index > -1)) { | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             if (this.previousFocusedImage) { | ||||
|                 // determine if the previous image exists in the new bounds of imageHistory | ||||
|                 const matchIndex = this.matchIndexOfPreviousImage( | ||||
|                     this.previousFocusedImage, | ||||
|                     this.imageHistory | ||||
|                 ); | ||||
|                 focusedIndex = matchIndex > -1 ? matchIndex : this.imageHistory.length - 1; | ||||
|  | ||||
|                 delete this.previousFocusedImage; | ||||
|             } | ||||
|  | ||||
|             if (thumbnailClick) { | ||||
|                 //We use the props till the user changes what they want to see | ||||
|                 this.initFocusedImageIndex = undefined; | ||||
|             } | ||||
|  | ||||
|             if (this.isPaused && !thumbnailClick && this.initFocusedImageIndex === undefined) { | ||||
|                 this.nextImageIndex = index; | ||||
|                 this.nextImageIndex = focusedIndex; | ||||
|                 //this could happen if bounds changes | ||||
|                 if (this.focusedImageIndex > this.imageHistory.length - 1) { | ||||
|                     this.focusedImageIndex = index; | ||||
|                     this.focusedImageIndex = focusedIndex; | ||||
|                 } | ||||
|  | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             this.focusedImageIndex = index; | ||||
|             this.focusedImageIndex = focusedIndex; | ||||
|  | ||||
|             if (thumbnailClick && !this.isPaused) { | ||||
|                 this.paused(true); | ||||
|   | ||||
| @@ -4,7 +4,7 @@ import NotebookSnapshotIndicator from './components/NotebookSnapshotIndicator.vu | ||||
| import SnapshotContainer from './snapshot-container'; | ||||
| import monkeyPatchObjectAPIForNotebooks from './monkeyPatchObjectAPIForNotebooks.js'; | ||||
|  | ||||
| import { notebookImageMigration } from '../notebook/utils/notebook-migration'; | ||||
| import { notebookImageMigration, IMAGE_MIGRATION_VER } from '../notebook/utils/notebook-migration'; | ||||
| import { NOTEBOOK_TYPE } from './notebook-constants'; | ||||
|  | ||||
| import Vue from 'vue'; | ||||
| @@ -28,6 +28,7 @@ export default function NotebookPlugin() { | ||||
|                 domainObject.configuration = { | ||||
|                     defaultSort: 'oldest', | ||||
|                     entries: {}, | ||||
|                     imageMigrationVer: IMAGE_MIGRATION_VER, | ||||
|                     pageTitle: 'Page', | ||||
|                     sections: [], | ||||
|                     sectionTitle: 'Section', | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| import { createNotebookImageDomainObject, getThumbnailURLFromimageUrl, saveNotebookImageDomainObject, updateNamespaceOfDomainObject } from './notebook-image'; | ||||
| import { mutateObject } from './notebook-entries'; | ||||
|  | ||||
| const IMAGE_MIGRATION_VER = "v1"; | ||||
| export const IMAGE_MIGRATION_VER = "v1"; | ||||
|  | ||||
| export function notebookImageMigration(openmct, domainObject) { | ||||
|     const configuration = domainObject.configuration; | ||||
|   | ||||
| @@ -108,7 +108,7 @@ describe('the plugin', () => { | ||||
|             expect(result).toBeTrue(); | ||||
|         }); | ||||
|  | ||||
|         it('updates an object', async () => { | ||||
|         xit('updates an object', async () => { | ||||
|             const result = await openmct.objects.save(mockDomainObject); | ||||
|             expect(result).toBeTrue(); | ||||
|             expect(provider.create).toHaveBeenCalled(); | ||||
|   | ||||
| @@ -31,8 +31,6 @@ | ||||
|         flex-direction: column; | ||||
|  | ||||
|         &--hidden { | ||||
|             height: 1000px; | ||||
|             width: 1000px; | ||||
|             position: absolute; | ||||
|             left: -9999px; | ||||
|             top: -9999px; | ||||
|   | ||||
| @@ -1,6 +1,10 @@ | ||||
| <template> | ||||
| <div class="c-tabs-view"> | ||||
| <div | ||||
|     ref="tabs" | ||||
|     class="c-tabs-view" | ||||
| > | ||||
|     <div | ||||
|         ref="tabsHolder" | ||||
|         class="c-tabs-view__tabs-holder c-tabs" | ||||
|         :class="{ | ||||
|             'is-dragging': isDragging && allowEditing, | ||||
| @@ -28,8 +32,10 @@ | ||||
|             }" | ||||
|             @click="showTab(tab, index)" | ||||
|         > | ||||
|             <div class="c-tabs-view__tab__label c-object-label" | ||||
|                  :class="[tab.status ? `is-status--${tab.status}` : '']" | ||||
|             <div | ||||
|                 ref="tabsLabel" | ||||
|                 class="c-tabs-view__tab__label c-object-label" | ||||
|                 :class="[tab.status ? `is-status--${tab.status}` : '']" | ||||
|             > | ||||
|                 <div class="c-object-label__type-icon" | ||||
|                      :class="tab.type.definition.cssClass" | ||||
| @@ -49,11 +55,12 @@ | ||||
|     <div | ||||
|         v-for="tab in tabsList" | ||||
|         :key="tab.keyString" | ||||
|         :style="getTabStyles(tab)" | ||||
|         class="c-tabs-view__object-holder" | ||||
|         :class="{'c-tabs-view__object-holder--hidden': !isCurrent(tab)}" | ||||
|     > | ||||
|         <object-view | ||||
|             v-if="isTabLoaded(tab)" | ||||
|             v-if="shouldLoadTab(tab)" | ||||
|             class="c-tabs-view__object" | ||||
|             :default-object="tab.domainObject" | ||||
|             :object-path="tab.objectPath" | ||||
| @@ -65,6 +72,7 @@ | ||||
| <script> | ||||
| import ObjectView from '../../../ui/components/ObjectView.vue'; | ||||
| import RemoveAction from '../../remove/RemoveAction.js'; | ||||
| import _ from 'lodash'; | ||||
|  | ||||
| const unknownObjectType = { | ||||
|     definition: { | ||||
| @@ -88,6 +96,8 @@ export default { | ||||
|         let keyString = this.openmct.objects.makeKeyString(this.domainObject.identifier); | ||||
|  | ||||
|         return { | ||||
|             tabWidth: undefined, | ||||
|             tabHeight: undefined, | ||||
|             internalDomainObject: this.domainObject, | ||||
|             currentTab: {}, | ||||
|             currentTabIndex: undefined, | ||||
| @@ -122,6 +132,10 @@ export default { | ||||
|             }); | ||||
|         } | ||||
|  | ||||
|         this.handleWindowResize = _.debounce(this.handleWindowResize, 500); | ||||
|         this.tabsViewResizeObserver = new ResizeObserver(this.handleWindowResize); | ||||
|         this.tabsViewResizeObserver.observe(this.$refs.tabs); | ||||
|  | ||||
|         this.unsubscribe = this.openmct.objects.observe(this.internalDomainObject, '*', this.updateInternalDomainObject); | ||||
|  | ||||
|         this.openmct.router.on('change:params', this.updateCurrentTab.bind(this)); | ||||
| @@ -138,6 +152,8 @@ export default { | ||||
|         this.composition.off('remove', this.removeItem); | ||||
|         this.composition.off('reorder', this.onReorder); | ||||
|  | ||||
|         this.tabsViewResizeObserver.disconnect(); | ||||
|  | ||||
|         this.tabsList.forEach(tab => { | ||||
|             tab.statusUnsubscribe(); | ||||
|         }); | ||||
| @@ -158,12 +174,28 @@ export default { | ||||
|  | ||||
|             this.loadedTabs[tab.keyString] = true; | ||||
|         }, | ||||
|         getTabStyles(tab) { | ||||
|             let styles = {}; | ||||
|  | ||||
|             if (!this.isCurrent(tab)) { | ||||
|                 styles = { | ||||
|                     height: this.tabHeight, | ||||
|                     width: this.tabWidth | ||||
|                 }; | ||||
|             } | ||||
|  | ||||
|             return styles; | ||||
|         }, | ||||
|         setCurrentTabByIndex(index) { | ||||
|             if (this.tabsList[index]) { | ||||
|                 this.showTab(this.tabsList[index]); | ||||
|             } | ||||
|         }, | ||||
|         showTab(tab, index) { | ||||
|             if (!tab) { | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             if (index !== undefined) { | ||||
|                 this.storeCurrentTabIndexInURL(index); | ||||
|             } | ||||
| @@ -171,6 +203,13 @@ export default { | ||||
|             this.currentTab = tab; | ||||
|             this.addTabToLoaded(tab); | ||||
|         }, | ||||
|         shouldLoadTab(tab) { | ||||
|             const isLoaded = this.isTabLoaded(tab); | ||||
|             const isCurrent = this.isCurrent(tab); | ||||
|             const tabElLoaded = this.tabWidth !== undefined && this.tabHeight !== undefined; | ||||
|  | ||||
|             return (isLoaded && isCurrent) || ((isLoaded && !isCurrent) && tabElLoaded); | ||||
|         }, | ||||
|         showRemoveDialog(index) { | ||||
|             if (!this.tabsList[index]) { | ||||
|                 return; | ||||
| @@ -325,6 +364,14 @@ export default { | ||||
|  | ||||
|             this.currentTabIndex = tabIndex; | ||||
|             this.currentTab = this.tabsList[tabIndex]; | ||||
|         }, | ||||
|         handleWindowResize() { | ||||
|             if (!this.$refs.tabs || !this.$refs.tabsHolder) { | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             this.tabWidth = this.$refs.tabs.offsetWidth + 'px'; | ||||
|             this.tabHeight = this.$refs.tabsHolder.offsetHeight - this.$refs.tabs.offsetHeight + 'px'; | ||||
|         } | ||||
|     } | ||||
| }; | ||||
|   | ||||
| @@ -20,7 +20,11 @@ | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| import { createOpenMct, resetApplicationState } from 'utils/testing'; | ||||
| import { | ||||
|     createOpenMct, | ||||
|     resetApplicationState, | ||||
|     spyOnBuiltins | ||||
| } from 'utils/testing'; | ||||
| import TabsLayout from './plugin'; | ||||
| import Vue from "vue"; | ||||
| import EventEmitter from "EventEmitter"; | ||||
| @@ -63,13 +67,13 @@ describe('the plugin', function () { | ||||
|             'phase': 5, | ||||
|             'randomness': 0 | ||||
|         }, | ||||
|         'name': 'Sine Wave Generator', | ||||
|         'type': 'generator', | ||||
|         'modified': 1592851063871, | ||||
|         'location': 'mine', | ||||
|         'persisted': 1592851063871 | ||||
|     }; | ||||
|     let telemetryItem1 = Object.assign({}, telemetryItemTemplate, { | ||||
|         'name': 'Sine Wave Generator 1', | ||||
|         'id': '55122607-e65e-44d5-9c9d-9c31a914ca89', | ||||
|         'identifier': { | ||||
|             'namespace': '', | ||||
| @@ -77,6 +81,7 @@ describe('the plugin', function () { | ||||
|         } | ||||
|     }); | ||||
|     let telemetryItem2 = Object.assign({}, telemetryItemTemplate, { | ||||
|         'name': 'Sine Wave Generator 2', | ||||
|         'id': '55122607-e65e-44d5-9c9d-9c31a914ca90', | ||||
|         'identifier': { | ||||
|             'namespace': '', | ||||
| @@ -91,6 +96,9 @@ describe('the plugin', function () { | ||||
|  | ||||
|         element = document.createElement('div'); | ||||
|         child = document.createElement('div'); | ||||
|         child.style.display = 'block'; | ||||
|         child.style.width = '1920px'; | ||||
|         child.style.height = '1080px'; | ||||
|         element.appendChild(child); | ||||
|  | ||||
|         openmct.on('start', done); | ||||
| @@ -150,8 +158,17 @@ describe('the plugin', function () { | ||||
|         let tabsLayoutViewProvider; | ||||
|         let mockComposition; | ||||
|         let count = 0; | ||||
|         let resizeCallback; | ||||
|  | ||||
|         beforeEach(() => { | ||||
|             class mockResizeObserver { | ||||
|                 constructor(cb) { | ||||
|                     resizeCallback = cb; | ||||
|                 } | ||||
|                 observe() { } | ||||
|                 disconnect() { } | ||||
|             } | ||||
|  | ||||
|             mockComposition = new EventEmitter(); | ||||
|             mockComposition.load = () => { | ||||
|                 if (count === 0) { | ||||
| @@ -165,6 +182,9 @@ describe('the plugin', function () { | ||||
|  | ||||
|             spyOn(openmct.composition, 'get').and.returnValue(mockComposition); | ||||
|  | ||||
|             spyOnBuiltins(['ResizeObserver']); | ||||
|             window.ResizeObserver.and.callFake(mockResizeObserver); | ||||
|  | ||||
|             const applicableViews = openmct.objectViews.get(testViewObject, []); | ||||
|             tabsLayoutViewProvider = applicableViews.find((viewProvider) => viewProvider.key === 'tabs'); | ||||
|             let view = tabsLayoutViewProvider.view(testViewObject, []); | ||||
| @@ -175,6 +195,7 @@ describe('the plugin', function () { | ||||
|  | ||||
|         afterEach(() => { | ||||
|             count = 0; | ||||
|             testViewObject.keep_alive = true; | ||||
|         }); | ||||
|  | ||||
|         it ('renders a tab for each item', () => { | ||||
| @@ -185,10 +206,22 @@ describe('the plugin', function () { | ||||
|  | ||||
|         describe('with domainObject.keep_alive set to', () => { | ||||
|  | ||||
|             it ('true, will keep all views loaded, regardless of current tab view', () => { | ||||
|                 let tabViewEls = element.querySelectorAll('.c-tabs-view__object'); | ||||
|             it ('true, will keep all views loaded, regardless of current tab view', (done) => { | ||||
|                 resizeCallback(); | ||||
|  | ||||
|                 expect(tabViewEls.length).toEqual(2); | ||||
|                 // the function called by the resize observer is debounced 500ms, | ||||
|                 // this is to account for that | ||||
|                 let promise = new Promise((resolve, reject) => { | ||||
|                     setTimeout(resolve, 501); | ||||
|                 }); | ||||
|  | ||||
|                 Promise.all([Vue.nextTick(), promise]).then(() => { | ||||
|                     let tabViewEls = element.querySelectorAll('.c-tabs-view__object'); | ||||
|  | ||||
|                     expect(tabViewEls.length).toEqual(2); | ||||
|                 }).finally(() => { | ||||
|                     done(); | ||||
|                 }); | ||||
|             }); | ||||
|  | ||||
|             it ('false, will only keep the current tab view loaded', async () => { | ||||
|   | ||||
| @@ -113,7 +113,6 @@ import search from '../components/search.vue'; | ||||
|  | ||||
| const ITEM_BUFFER = 25; | ||||
| const LOCAL_STORAGE_KEY__TREE_EXPANDED = 'mct-tree-expanded'; | ||||
| const RETURN_ALL_DESCENDANTS = true; | ||||
| const SORT_MY_ITEMS_ALPH_ASC = true; | ||||
| const TREE_ITEM_INDENT_PX = 18; | ||||
|  | ||||
| @@ -432,7 +431,7 @@ export default { | ||||
|  | ||||
|             return scrollTopAmount >= treeStart && scrollTopAmount < treeEnd; | ||||
|         }, | ||||
|         sortNameDescending(a, b) { | ||||
|         sortNameAscending(a, b) { | ||||
|             // sorting tree children items | ||||
|             if (!(a.name && b.name)) { | ||||
|                 if (a.object.name > b.object.name) { | ||||
| @@ -455,15 +454,16 @@ export default { | ||||
|  | ||||
|             return 0; | ||||
|         }, | ||||
|  | ||||
|         isSortable(parentObjectPath) { | ||||
|             // determine if any part of the parent's path includes a key value of mine; aka My Items | ||||
|             return Boolean(parentObjectPath.find(path => path.identifier.key === 'mine')); | ||||
|         }, | ||||
|         async loadAndBuildTreeItemsFor(domainObject, parentObjectPath, abortSignal) { | ||||
|             let collection = this.openmct.composition.get(domainObject); | ||||
|             let composition = await collection.load(abortSignal); | ||||
|             // determine if any part of the parent's path includes a key value of mine; aka My Items | ||||
|             const isNestedInMyItems = Boolean(parentObjectPath.find(path => path.identifier.key === 'mine')); | ||||
|  | ||||
|             if (SORT_MY_ITEMS_ALPH_ASC && isNestedInMyItems) { | ||||
|                 const sortedComposition = composition.sort(this.sortNameDescending); | ||||
|             if (SORT_MY_ITEMS_ALPH_ASC && this.isSortable(parentObjectPath)) { | ||||
|                 const sortedComposition = composition.slice().sort(this.sortNameAscending); | ||||
|                 composition = sortedComposition; | ||||
|             } | ||||
|  | ||||
| @@ -509,17 +509,35 @@ export default { | ||||
|         }, | ||||
|         compositionAddHandler(navigationPath) { | ||||
|             return (domainObject) => { | ||||
|                 let parentItem = this.getTreeItemByPath(navigationPath); | ||||
|                 let newItem = this.buildTreeItem(domainObject, parentItem.objectPath, true); | ||||
|                 let allDescendants = this.getChildrenInTreeFor(parentItem, RETURN_ALL_DESCENDANTS); | ||||
|                 let afterItem = allDescendants.length ? allDescendants.pop() : parentItem; | ||||
|                 const parentItem = this.getTreeItemByPath(navigationPath); | ||||
|                 const newItem = this.buildTreeItem(domainObject, parentItem.objectPath, true); | ||||
|                 const descendants = this.getChildrenInTreeFor(parentItem, true); | ||||
|                 const directDescendants = this.getChildrenInTreeFor(parentItem); | ||||
|  | ||||
|                 this.addItemToTreeAfter(newItem, afterItem); | ||||
|                 const isNestedInMyItems = Boolean(parentItem.objectPath && parentItem.objectPath.find(path => path.identifier.key === 'mine')); | ||||
|                 if (directDescendants.length === 0) { | ||||
|                     this.addItemToTreeAfter(newItem, parentItem); | ||||
|  | ||||
|                 if (SORT_MY_ITEMS_ALPH_ASC && isNestedInMyItems) { | ||||
|                     this.sortTreeComposition(this.sortNameDescending, navigationPath); | ||||
|                     return; | ||||
|                 } | ||||
|  | ||||
|                 if (SORT_MY_ITEMS_ALPH_ASC && this.isSortable(parentItem.objectPath)) { | ||||
|                     const newItemIndex = directDescendants | ||||
|                         .findIndex(descendant => this.sortNameAscending(descendant, newItem) > 0); | ||||
|                     const shouldInsertFirst = newItemIndex === 0; | ||||
|                     const shouldInsertLast = newItemIndex === -1; | ||||
|  | ||||
|                     if (shouldInsertFirst) { | ||||
|                         this.addItemToTreeAfter(newItem, parentItem); | ||||
|                     } else if (shouldInsertLast) { | ||||
|                         this.addItemToTreeAfter(newItem, descendants.pop()); | ||||
|                     } else { | ||||
|                         this.addItemToTreeBefore(newItem, directDescendants[newItemIndex]); | ||||
|                     } | ||||
|  | ||||
|                     return; | ||||
|                 } | ||||
|  | ||||
|                 this.addItemToTreeAfter(newItem, descendants.pop()); | ||||
|             }; | ||||
|         }, | ||||
|         compositionRemoveHandler(navigationPath) { | ||||
| @@ -551,17 +569,18 @@ export default { | ||||
|             const removeIndex = this.getTreeItemIndex(item.navigationPath); | ||||
|             this.treeItems.splice(removeIndex, 1); | ||||
|         }, | ||||
|         sortTreeComposition(algorithem, parentPath) { | ||||
|             const parentIndex = this.getTreeItemIndex(parentPath); | ||||
|             const parentItem = this.treeItems[parentIndex]; | ||||
|         addItemToTreeBefore(addItem, beforeItem) { | ||||
|             const addIndex = this.getTreeItemIndex(beforeItem.navigationPath); | ||||
|  | ||||
|             const allDescendants = this.getChildrenInTreeFor(parentItem); | ||||
|             const sortedChildren = allDescendants.sort(algorithem); | ||||
|             this.treeItems.splice(parentIndex + 1, allDescendants.length, ...sortedChildren); | ||||
|             this.addItemToTree(addItem, addIndex); | ||||
|         }, | ||||
|         addItemToTreeAfter(addItem, afterItem) { | ||||
|             const addIndex = this.getTreeItemIndex(afterItem.navigationPath); | ||||
|             this.treeItems.splice(addIndex + 1, 0, addItem); | ||||
|  | ||||
|             this.addItemToTree(addItem, addIndex + 1); | ||||
|         }, | ||||
|         addItemToTree(addItem, index) { | ||||
|             this.treeItems.splice(index, 0, addItem); | ||||
|  | ||||
|             if (this.isTreeItemOpen(addItem)) { | ||||
|                 this.openTreeItem(addItem); | ||||
|   | ||||
		Reference in New Issue
	
	Block a user