Compare commits
	
		
			9 Commits
		
	
	
		
			v4.0.0-rc2
			...
			mct4555
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					370fdc6466 | ||
| 
						 | 
					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());
 | 
			
		||||
 
 | 
			
		||||
@@ -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,9 +414,11 @@ export default {
 | 
			
		||||
            if (this.indexForFocusedImage !== undefined) {
 | 
			
		||||
                imageIndex = this.initFocusedImageIndex;
 | 
			
		||||
            } else {
 | 
			
		||||
                imageIndex = newSize - 1;
 | 
			
		||||
                imageIndex = newSize > 0 ? newSize - 1 : undefined;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // handle subscription update
 | 
			
		||||
            this.previousFocusedImage = this.focusedImage ? JSON.parse(JSON.stringify(this.focusedImage)) : undefined;
 | 
			
		||||
            this.setFocusedImage(imageIndex, false);
 | 
			
		||||
            this.scrollToRight();
 | 
			
		||||
        },
 | 
			
		||||
@@ -670,24 +672,49 @@ 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) {
 | 
			
		||||
                console.log('previousImageUrl', this.previousFocusedImage.url)
 | 
			
		||||
                // 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;
 | 
			
		||||
                console.log('new focused image', focusedIndex, this.imageHistory[focusedIndex].url)
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (thumbnailClick) {
 | 
			
		||||
                //We use the props till the user changes what they want to see
 | 
			
		||||
                this.initFocusedImageIndex = undefined;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            this.focusedImageIndex = focusedIndex;
 | 
			
		||||
            delete this.previousFocusedImage;
 | 
			
		||||
 | 
			
		||||
            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;
 | 
			
		||||
 | 
			
		||||
            if (thumbnailClick && !this.isPaused) {
 | 
			
		||||
                this.paused(true);
 | 
			
		||||
            }
 | 
			
		||||
 
 | 
			
		||||
@@ -122,14 +122,46 @@ export default {
 | 
			
		||||
            return this.timeFormatter.parse(datum);
 | 
			
		||||
        },
 | 
			
		||||
        boundsChange(bounds, isTick) {
 | 
			
		||||
            if (!isTick) {
 | 
			
		||||
                this.requestHistory();
 | 
			
		||||
            if (isTick) {
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // the focusedImageIndex is calculated twice; once before and after request history arrives
 | 
			
		||||
            const focusedImage = this.imageHistory[this.focusedImageIndex];
 | 
			
		||||
            const previousFocusedImage = focusedImage ? JSON.parse(JSON.stringify(focusedImage)) : undefined;
 | 
			
		||||
            this.updateFocusedImageIndex(previousFocusedImage, this.imageHistory);
 | 
			
		||||
 | 
			
		||||
            return this.requestHistory();
 | 
			
		||||
        },
 | 
			
		||||
        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
 | 
			
		||||
                && x.utc === previous.utc
 | 
			
		||||
            ));
 | 
			
		||||
        },
 | 
			
		||||
        updateFocusedImageIndex(previous, imageHistory) {
 | 
			
		||||
            if (!previous) {
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            const matchIndex = this.matchIndexOfPreviousImage(
 | 
			
		||||
                previous,
 | 
			
		||||
                imageHistory
 | 
			
		||||
            );
 | 
			
		||||
            this.focusedImageIndex = matchIndex > -1 ? matchIndex : this.imageHistory.length - 1;
 | 
			
		||||
 | 
			
		||||
        },
 | 
			
		||||
        async requestHistory() {
 | 
			
		||||
            let bounds = this.timeContext.bounds();
 | 
			
		||||
            this.requestCount++;
 | 
			
		||||
            const requestId = this.requestCount;
 | 
			
		||||
 | 
			
		||||
            // maintain previous focused image value to discern new index
 | 
			
		||||
            const focusedImage = this.imageHistory[this.focusedImageIndex];
 | 
			
		||||
            const previousFocusedImage = focusedImage ? JSON.parse(JSON.stringify(focusedImage)) : undefined;
 | 
			
		||||
            this.imageHistory = [];
 | 
			
		||||
 | 
			
		||||
            let data = await this.openmct.telemetry
 | 
			
		||||
@@ -143,7 +175,8 @@ export default {
 | 
			
		||||
                        imagery.push(image);
 | 
			
		||||
                    }
 | 
			
		||||
                });
 | 
			
		||||
                //this is to optimize anything that reacts to imageHistory length
 | 
			
		||||
                this.updateFocusedImageIndex(previousFocusedImage, imagery);
 | 
			
		||||
 | 
			
		||||
                this.imageHistory = imagery;
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
 
 | 
			
		||||
@@ -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