Compare commits
	
		
			9 Commits
		
	
	
		
			logrocket-
			...
			fix-plots-
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | 200e8eecce | ||
|   | 7a1e7edb79 | ||
|   | e1e0eeac56 | ||
|   | c90dfb2a1f | ||
|   | 53232a1c70 | ||
|   | 35b6952cc1 | ||
|   | 1dfa5e5b8c | ||
|   | 99896b72ea | ||
|   | 979ba77c8e | 
							
								
								
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -39,5 +39,3 @@ npm-debug.log | ||||
|  | ||||
| # karma reports | ||||
| report.*.json | ||||
|  | ||||
| package-lock.json | ||||
|   | ||||
							
								
								
									
										2
									
								
								API.md
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								API.md
									
									
									
									
									
								
							| @@ -423,7 +423,7 @@ attribute      | type   | flags    | notes | ||||
|  | ||||
| ###### Value Hints | ||||
|  | ||||
| Each telemetry value description has an object defining hints.  Keys in this this object represent the hint itself, and the value represents the weight of that hint.  A lower weight means the hint has a higher priority.  For example, multiple values could be hinted for use as the y-axis of a plot (raw, engineering), but the highest priority would be the default choice.  Likewise, a table will use hints to determine the default order of columns. | ||||
| Each telemetry value description has an object defining hints.  Keys in this object represent the hint itself, and the value represents the weight of that hint.  A lower weight means the hint has a higher priority.  For example, multiple values could be hinted for use as the y-axis of a plot (raw, engineering), but the highest priority would be the default choice.  Likewise, a table will use hints to determine the default order of columns. | ||||
|  | ||||
| Known hints: | ||||
|  | ||||
|   | ||||
							
								
								
									
										10
									
								
								app.js
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								app.js
									
									
									
									
									
								
							| @@ -7,6 +7,14 @@ | ||||
|  * node app.js [options] | ||||
|  */ | ||||
|  | ||||
| class WatchRunPlugin { | ||||
|     apply(compiler) { | ||||
|         compiler.hooks.emit.tapAsync('WatchRunPlugin', (compilation, callback) => { | ||||
|             console.log('Begin compile at ' + new Date()); | ||||
|             callback(); | ||||
|         }); | ||||
|     } | ||||
| } | ||||
|  | ||||
| const options = require('minimist')(process.argv.slice(2)); | ||||
| const express = require('express'); | ||||
| @@ -43,7 +51,7 @@ app.use('/proxyUrl', function proxyRequest(req, res, next) { | ||||
| const webpack = require('webpack'); | ||||
| const webpackConfig = require('./webpack.config.js'); | ||||
| webpackConfig.plugins.push(new webpack.HotModuleReplacementPlugin()); | ||||
| webpackConfig.plugins.push(function() { this.plugin('watch-run', function(watching, callback) { console.log('Begin compile at ' + new Date()); callback(); }) }); | ||||
| webpackConfig.plugins.push(new WatchRunPlugin()); | ||||
|  | ||||
| webpackConfig.entry.openmct = [ | ||||
|     'webpack-hot-middleware/client?reload=true', | ||||
|   | ||||
| @@ -78,6 +78,7 @@ module.exports = (config) => { | ||||
|             preserveDescribeNesting: true, | ||||
|             foldAll: false | ||||
|         }, | ||||
|         browserConsoleLogOptions: { level: "error",  format: "%b %T: %m",  terminal: true }, | ||||
|         coverageIstanbulReporter: { | ||||
|             fixWebpackSourcePaths: true, | ||||
|             dir: process.env.CIRCLE_ARTIFACTS ? | ||||
|   | ||||
| @@ -21,11 +21,6 @@ | ||||
|  *****************************************************************************/ | ||||
| /*global module*/ | ||||
|  | ||||
| const LogRocket = require('logrocket'); | ||||
|  | ||||
| // Instrumented | ||||
| LogRocket.init('omjbg1/openmct-demo'); | ||||
|  | ||||
| const matcher = /\/openmct.js$/; | ||||
| if (document.currentScript) { | ||||
|     let src = document.currentScript.src; | ||||
|   | ||||
							
								
								
									
										8613
									
								
								package-lock.json
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										8613
									
								
								package-lock.json
									
									
									
										generated
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										23
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										23
									
								
								package.json
									
									
									
									
									
								
							| @@ -1,17 +1,15 @@ | ||||
| { | ||||
|   "name": "openmct", | ||||
|   "version": "1.7.1-SNAPSHOT", | ||||
|   "version": "1.7.3-SNAPSHOT", | ||||
|   "description": "The Open MCT core platform", | ||||
|   "dependencies": { | ||||
|     "logrocket": "^1.2.0" | ||||
|   }, | ||||
|   "dependencies": {}, | ||||
|   "devDependencies": { | ||||
|     "angular": ">=1.8.0", | ||||
|     "angular-route": "1.4.14", | ||||
|     "babel-eslint": "10.0.3", | ||||
|     "comma-separated-values": "^3.6.4", | ||||
|     "concurrently": "^3.6.1", | ||||
|     "copy-webpack-plugin": "^4.5.2", | ||||
|     "copy-webpack-plugin": "^9.0.0", | ||||
|     "cross-env": "^6.0.3", | ||||
|     "css-loader": "^1.0.0", | ||||
|     "d3-array": "1.2.x", | ||||
| @@ -43,19 +41,19 @@ | ||||
|     "jsdoc": "^3.3.2", | ||||
|     "karma": "5.1.1", | ||||
|     "karma-chrome-launcher": "3.1.0", | ||||
|     "karma-firefox-launcher": "1.3.0", | ||||
|     "karma-cli": "2.0.0", | ||||
|     "karma-coverage": "2.0.3", | ||||
|     "karma-coverage-istanbul-reporter": "3.0.3", | ||||
|     "karma-firefox-launcher": "1.3.0", | ||||
|     "karma-html-reporter": "0.2.7", | ||||
|     "karma-jasmine": "3.3.1", | ||||
|     "karma-sourcemap-loader": "0.3.7", | ||||
|     "karma-webpack": "4.0.2", | ||||
|     "karma-webpack": "^5.0.0", | ||||
|     "location-bar": "^3.0.1", | ||||
|     "lodash": "^4.17.12", | ||||
|     "markdown-toc": "^0.11.7", | ||||
|     "marked": "^0.3.5", | ||||
|     "mini-css-extract-plugin": "^0.4.1", | ||||
|     "mini-css-extract-plugin": "^1.6.0", | ||||
|     "minimist": "^1.2.5", | ||||
|     "moment": "2.25.3", | ||||
|     "moment-duration-format": "^2.2.2", | ||||
| @@ -71,16 +69,17 @@ | ||||
|     "uuid": "^3.3.3", | ||||
|     "v8-compile-cache": "^1.1.0", | ||||
|     "vue": "2.5.6", | ||||
|     "vue-loader": "^15.2.6", | ||||
|     "vue-loader": "^15.9.7", | ||||
|     "vue-template-compiler": "2.5.6", | ||||
|     "webpack": "^4.16.2", | ||||
|     "webpack-cli": "^3.1.0", | ||||
|     "webpack": "^5.37.0", | ||||
|     "webpack-cli": "^3.3.12", | ||||
|     "webpack-dev-middleware": "^3.1.3", | ||||
|     "webpack-hot-middleware": "^2.22.3", | ||||
|     "zepto": "^1.2.0" | ||||
|   }, | ||||
|   "scripts": { | ||||
|     "clean": "rm -rf ./dist", | ||||
|     "clean": "rm -rf ./dist /node_modules; rm package-lock.json", | ||||
|     "clean-test-lint": "npm run clean; npm install ; npm run test; npm run lint", | ||||
|     "start": "node app.js", | ||||
|     "lint": "eslint platform example src --ext .js,.vue openmct.js", | ||||
|     "lint:fix": "eslint platform example src --ext .js,.vue openmct.js --fix", | ||||
|   | ||||
| @@ -86,7 +86,7 @@ define( | ||||
|                         }) | ||||
|                         .join('/'); | ||||
|  | ||||
|                 window.location.href = url; | ||||
|                 openmct.router.navigate(url); | ||||
|  | ||||
|                 if (isFirstViewEditable(object.useCapability('adapter'), objectPath)) { | ||||
|                     openmct.editor.edit(); | ||||
|   | ||||
| @@ -252,7 +252,7 @@ define([ | ||||
|  | ||||
|         this.status = new api.StatusAPI(this); | ||||
|  | ||||
|         this.router = new ApplicationRouter(); | ||||
|         this.router = new ApplicationRouter(this); | ||||
|  | ||||
|         this.branding = BrandingAPI.default; | ||||
|  | ||||
|   | ||||
| @@ -119,7 +119,8 @@ describe('The ActionCollection', () => { | ||||
|  | ||||
|     afterEach(() => { | ||||
|         actionCollection.destroy(); | ||||
|         resetApplicationState(openmct); | ||||
|  | ||||
|         return resetApplicationState(openmct); | ||||
|     }); | ||||
|  | ||||
|     describe("disable method invoked with action keys", () => { | ||||
|   | ||||
| @@ -99,7 +99,7 @@ describe('The Actions API', () => { | ||||
|     }); | ||||
|  | ||||
|     afterEach(() => { | ||||
|         resetApplicationState(openmct); | ||||
|         return resetApplicationState(openmct); | ||||
|     }); | ||||
|  | ||||
|     describe("register method", () => { | ||||
|   | ||||
| @@ -76,7 +76,7 @@ describe ('The Menu API', () => { | ||||
|     }); | ||||
|  | ||||
|     afterEach(() => { | ||||
|         resetApplicationState(openmct); | ||||
|         return resetApplicationState(openmct); | ||||
|     }); | ||||
|  | ||||
|     describe("showMenu method", () => { | ||||
|   | ||||
| @@ -22,7 +22,7 @@ describe("The Status API", () => { | ||||
|     }); | ||||
|  | ||||
|     afterEach(() => { | ||||
|         resetApplicationState(openmct); | ||||
|         return resetApplicationState(openmct); | ||||
|     }); | ||||
|  | ||||
|     describe("set function", () => { | ||||
|   | ||||
| @@ -292,6 +292,11 @@ describe("The LAD Table Set", () => { | ||||
|     }); | ||||
|  | ||||
|     afterEach(() => { | ||||
|         openmct.time.timeSystem('utc', { | ||||
|             start: 0, | ||||
|             end: 1 | ||||
|         }); | ||||
|  | ||||
|         return resetApplicationState(openmct); | ||||
|     }); | ||||
|  | ||||
|   | ||||
| @@ -19,10 +19,6 @@ | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
| import { | ||||
|     getAllSearchParams, | ||||
|     setAllSearchParams | ||||
| } from 'utils/openmctLocation'; | ||||
|  | ||||
| const TIME_EVENTS = ['timeSystem', 'clock', 'clockOffsets']; | ||||
| const SEARCH_MODE = 'tc.mode'; | ||||
| @@ -49,9 +45,8 @@ export default class URLTimeSettingsSynchronizer { | ||||
|     } | ||||
|  | ||||
|     initialize() { | ||||
|         this.updateTimeSettings(); | ||||
|         this.openmct.router.on('change:params', this.updateTimeSettings); | ||||
|  | ||||
|         window.addEventListener('hashchange', this.updateTimeSettings); | ||||
|         TIME_EVENTS.forEach(event => { | ||||
|             this.openmct.time.on(event, this.setUrlFromTimeApi); | ||||
|         }); | ||||
| @@ -59,7 +54,8 @@ export default class URLTimeSettingsSynchronizer { | ||||
|     } | ||||
|  | ||||
|     destroy() { | ||||
|         window.removeEventListener('hashchange', this.updateTimeSettings); | ||||
|         this.openmct.router.off('change:params', this.updateTimeSettings); | ||||
|  | ||||
|         this.openmct.off('start', this.initialize); | ||||
|         this.openmct.off('destroy', this.destroy); | ||||
|  | ||||
| @@ -70,22 +66,18 @@ export default class URLTimeSettingsSynchronizer { | ||||
|     } | ||||
|  | ||||
|     updateTimeSettings() { | ||||
|         // Prevent from triggering self | ||||
|         if (!this.isUrlUpdateInProgress) { | ||||
|             let timeParameters = this.parseParametersFromUrl(); | ||||
|         let timeParameters = this.parseParametersFromUrl(); | ||||
|  | ||||
|             if (this.areTimeParametersValid(timeParameters)) { | ||||
|                 this.setTimeApiFromUrl(timeParameters); | ||||
|             } else { | ||||
|                 this.setUrlFromTimeApi(); | ||||
|             } | ||||
|         if (this.areTimeParametersValid(timeParameters)) { | ||||
|             this.setTimeApiFromUrl(timeParameters); | ||||
|             this.openmct.router.setLocationFromUrl(); | ||||
|         } else { | ||||
|             this.isUrlUpdateInProgress = false; | ||||
|             this.setUrlFromTimeApi(); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     parseParametersFromUrl() { | ||||
|         let searchParams = getAllSearchParams(); | ||||
|         let searchParams = this.openmct.router.getAllSearchParams(); | ||||
|  | ||||
|         let mode = searchParams.get(SEARCH_MODE); | ||||
|         let timeSystem = searchParams.get(SEARCH_TIME_SYSTEM); | ||||
| @@ -148,7 +140,7 @@ export default class URLTimeSettingsSynchronizer { | ||||
|     } | ||||
|  | ||||
|     setUrlFromTimeApi() { | ||||
|         let searchParams = getAllSearchParams(); | ||||
|         let searchParams = this.openmct.router.getAllSearchParams(); | ||||
|         let clock = this.openmct.time.clock(); | ||||
|         let bounds = this.openmct.time.bounds(); | ||||
|         let clockOffsets = this.openmct.time.clockOffsets(); | ||||
| @@ -176,8 +168,7 @@ export default class URLTimeSettingsSynchronizer { | ||||
|         } | ||||
|  | ||||
|         searchParams.set(SEARCH_TIME_SYSTEM, this.openmct.time.timeSystem().key); | ||||
|         this.isUrlUpdateInProgress = true; | ||||
|         setAllSearchParams(searchParams); | ||||
|         this.openmct.router.setAllSearchParams(searchParams); | ||||
|     } | ||||
|  | ||||
|     areTimeParametersValid(timeParameters) { | ||||
|   | ||||
| @@ -25,306 +25,118 @@ import { | ||||
| } from 'utils/testing'; | ||||
|  | ||||
| describe("The URLTimeSettingsSynchronizer", () => { | ||||
|     let appHolder; | ||||
|     let openmct; | ||||
|     let testClock; | ||||
|     let resolveFunction; | ||||
|     let oldHash; | ||||
|  | ||||
|     beforeEach((done) => { | ||||
|         openmct = createOpenMct(); | ||||
|         openmct.install(openmct.plugins.MyItems()); | ||||
|         openmct.install(openmct.plugins.LocalTimeSystem()); | ||||
|         testClock = jasmine.createSpyObj("testClock", ["start", "stop", "tick", "currentValue", "on", "off"]); | ||||
|         testClock.key = "test-clock"; | ||||
|         testClock.currentValue.and.returnValue(0); | ||||
|  | ||||
|         openmct.time.addClock(testClock); | ||||
|         openmct.install(openmct.plugins.UTCTimeSystem()); | ||||
|  | ||||
|         openmct.on('start', done); | ||||
|         openmct.startHeadless(); | ||||
|  | ||||
|         appHolder = document.createElement("div"); | ||||
|         openmct.start(appHolder); | ||||
|     }); | ||||
|  | ||||
|     afterEach(() => resetApplicationState(openmct)); | ||||
|     afterEach(() => { | ||||
|         openmct.time.stopClock(); | ||||
|         openmct.router.removeListener('change:hash', resolveFunction); | ||||
|  | ||||
|     describe("realtime mode", () => { | ||||
|         it("when the clock is set via the time API, it is immediately reflected in the URL", () => { | ||||
|             //Test expected initial conditions | ||||
|         appHolder = undefined; | ||||
|         openmct = undefined; | ||||
|         resolveFunction = undefined; | ||||
|  | ||||
|         return resetApplicationState(openmct); | ||||
|     }); | ||||
|  | ||||
|     it("initial clock is set to fixed is reflected in URL", (done) => { | ||||
|         resolveFunction = () => { | ||||
|             oldHash = window.location.hash; | ||||
|             expect(window.location.hash.includes('tc.mode=fixed')).toBe(true); | ||||
|  | ||||
|             openmct.router.removeListener('change:hash', resolveFunction); | ||||
|             done(); | ||||
|         }; | ||||
|  | ||||
|         openmct.router.on('change:hash', resolveFunction); | ||||
|     }); | ||||
|  | ||||
|     it("when the clock is set via the time API, it is reflected in the URL", (done) => { | ||||
|         let success; | ||||
|  | ||||
|         resolveFunction = () => { | ||||
|             openmct.time.clock('local', { | ||||
|                 start: -1000, | ||||
|                 end: 100 | ||||
|             }); | ||||
|  | ||||
|             expect(window.location.hash.includes('tc.mode=local')).toBe(true); | ||||
|  | ||||
|             //Test that expected initial conditions are no longer true | ||||
|             expect(window.location.hash.includes('tc.mode=fixed')).toBe(false); | ||||
|         }); | ||||
|         it("when offsets are set via the time API, they are immediately reflected in the URL", () => { | ||||
|             //Test expected initial conditions | ||||
|             expect(window.location.hash.includes('tc.startDelta')).toBe(false); | ||||
|             expect(window.location.hash.includes('tc.endDelta')).toBe(false); | ||||
|  | ||||
|             openmct.time.clock('local', { | ||||
|                 start: -1000, | ||||
|                 end: 100 | ||||
|             }); | ||||
|             expect(window.location.hash.includes('tc.startDelta=1000')).toBe(true); | ||||
|             expect(window.location.hash.includes('tc.endDelta=100')).toBe(true); | ||||
|  | ||||
|             openmct.time.clockOffsets({ | ||||
|                 start: -2000, | ||||
|                 end: 200 | ||||
|             }); | ||||
|             expect(window.location.hash.includes('tc.startDelta=2000')).toBe(true); | ||||
|             expect(window.location.hash.includes('tc.endDelta=200')).toBe(true); | ||||
|  | ||||
|             //Test that expected initial conditions are no longer true | ||||
|             expect(window.location.hash.includes('tc.mode=fixed')).toBe(false); | ||||
|         }); | ||||
|         describe("when set in the url", () => { | ||||
|             it("will change from fixed to realtime mode when the mode changes", () => { | ||||
|                 expectLocationToBeInFixedMode(); | ||||
|             const hasStartDelta = window.location.hash.includes('tc.startDelta=2000'); | ||||
|             const hasEndDelta = window.location.hash.includes('tc.endDelta=200'); | ||||
|             const hasLocalClock = window.location.hash.includes('tc.mode=local'); | ||||
|             success = hasStartDelta && hasEndDelta && hasLocalClock; | ||||
|             if (success) { | ||||
|                 expect(success).toBe(true); | ||||
|  | ||||
|                 return switchToRealtimeMode().then(() => { | ||||
|                     let clock = openmct.time.clock(); | ||||
|                 openmct.router.removeListener('change:hash', resolveFunction); | ||||
|                 done(); | ||||
|             } | ||||
|         }; | ||||
|  | ||||
|                     expect(clock).toBeDefined(); | ||||
|                     expect(clock.key).toBe('local'); | ||||
|                 }); | ||||
|             }); | ||||
|             it("the clock is correctly set in the API from the URL parameters", () => { | ||||
|                 return switchToRealtimeMode().then(() => { | ||||
|                     let resolveFunction; | ||||
|  | ||||
|                     return new Promise((resolve) => { | ||||
|                         resolveFunction = resolve; | ||||
|  | ||||
|                         //The 'hashchange' event appears to be asynchronous, so we need to wait until a clock change has been | ||||
|                         //detected in the API. | ||||
|                         openmct.time.on('clock', resolveFunction); | ||||
|                         let hash = window.location.hash; | ||||
|                         hash = hash.replace('tc.mode=local', 'tc.mode=test-clock'); | ||||
|                         window.location.hash = hash; | ||||
|                     }).then(() => { | ||||
|                         let clock = openmct.time.clock(); | ||||
|                         expect(clock).toBeDefined(); | ||||
|                         expect(clock.key).toBe('test-clock'); | ||||
|                         openmct.time.off('clock', resolveFunction); | ||||
|                     }); | ||||
|                 }); | ||||
|             }); | ||||
|             it("the clock offsets are correctly set in the API from the URL parameters", () => { | ||||
|                 return switchToRealtimeMode().then(() => { | ||||
|                     let resolveFunction; | ||||
|  | ||||
|                     return new Promise((resolve) => { | ||||
|                         resolveFunction = resolve; | ||||
|                         //The 'hashchange' event appears to be asynchronous, so we need to wait until a clock change has been | ||||
|                         //detected in the API. | ||||
|                         openmct.time.on('clockOffsets', resolveFunction); | ||||
|                         let hash = window.location.hash; | ||||
|                         hash = hash.replace('tc.startDelta=1000', 'tc.startDelta=2000'); | ||||
|                         hash = hash.replace('tc.endDelta=100', 'tc.endDelta=200'); | ||||
|                         window.location.hash = hash; | ||||
|                     }).then(() => { | ||||
|                         let clockOffsets = openmct.time.clockOffsets(); | ||||
|                         expect(clockOffsets).toBeDefined(); | ||||
|                         expect(clockOffsets.start).toBe(-2000); | ||||
|                         expect(clockOffsets.end).toBe(200); | ||||
|                         openmct.time.off('clockOffsets', resolveFunction); | ||||
|                     }); | ||||
|                 }); | ||||
|             }); | ||||
|             it("the time system is correctly set in the API from the URL parameters", () => { | ||||
|                 return switchToRealtimeMode().then(() => { | ||||
|                     let resolveFunction; | ||||
|  | ||||
|                     return new Promise((resolve) => { | ||||
|                         resolveFunction = resolve; | ||||
|  | ||||
|                         //The 'hashchange' event appears to be asynchronous, so we need to wait until a clock change has been | ||||
|                         //detected in the API. | ||||
|                         openmct.time.on('timeSystem', resolveFunction); | ||||
|                         let hash = window.location.hash; | ||||
|                         hash = hash.replace('tc.timeSystem=utc', 'tc.timeSystem=local'); | ||||
|                         window.location.hash = hash; | ||||
|                     }).then(() => { | ||||
|                         let timeSystem = openmct.time.timeSystem(); | ||||
|                         expect(timeSystem).toBeDefined(); | ||||
|                         expect(timeSystem.key).toBe('local'); | ||||
|                         openmct.time.off('timeSystem', resolveFunction); | ||||
|                     }); | ||||
|                 }); | ||||
|             }); | ||||
|         }); | ||||
|     }); | ||||
|     describe("fixed timespan mode", () => { | ||||
|         beforeEach(() => { | ||||
|             openmct.time.stopClock(); | ||||
|             openmct.time.timeSystem('utc', { | ||||
|                 start: 0, | ||||
|                 end: 1 | ||||
|             }); | ||||
|         }); | ||||
|  | ||||
|         it("when bounds are set via the time API, they are immediately reflected in the URL", () => { | ||||
|             //Test expected initial conditions | ||||
|             expect(window.location.hash.includes('tc.startBound=0')).toBe(true); | ||||
|             expect(window.location.hash.includes('tc.endBound=1')).toBe(true); | ||||
|  | ||||
|             openmct.time.bounds({ | ||||
|                 start: 10, | ||||
|                 end: 20 | ||||
|             }); | ||||
|  | ||||
|             expect(window.location.hash.includes('tc.startBound=10')).toBe(true); | ||||
|             expect(window.location.hash.includes('tc.endBound=20')).toBe(true); | ||||
|  | ||||
|             //Test that expected initial conditions are no longer true | ||||
|             expect(window.location.hash.includes('tc.startBound=0')).toBe(false); | ||||
|             expect(window.location.hash.includes('tc.endBound=1')).toBe(false); | ||||
|         }); | ||||
|  | ||||
|         it("when time system is set via the time API, it is immediately reflected in the URL", () => { | ||||
|             //Test expected initial conditions | ||||
|             expect(window.location.hash.includes('tc.timeSystem=utc')).toBe(true); | ||||
|  | ||||
|             openmct.time.timeSystem('local', { | ||||
|                 start: 20, | ||||
|                 end: 30 | ||||
|             }); | ||||
|  | ||||
|             expect(window.location.hash.includes('tc.timeSystem=local')).toBe(true); | ||||
|  | ||||
|             //Test that expected initial conditions are no longer true | ||||
|             expect(window.location.hash.includes('tc.timeSystem=utc')).toBe(false); | ||||
|         }); | ||||
|         describe("when set in the url", () => { | ||||
|             it("time system changes are reflected in the API", () => { | ||||
|                 let resolveFunction; | ||||
|  | ||||
|                 return new Promise((resolve) => { | ||||
|                     let timeSystem = openmct.time.timeSystem(); | ||||
|                     resolveFunction = resolve; | ||||
|  | ||||
|                     expect(timeSystem.key).toBe('utc'); | ||||
|                     window.location.hash = window.location.hash.replace('tc.timeSystem=utc', 'tc.timeSystem=local'); | ||||
|  | ||||
|                     openmct.time.on('timeSystem', resolveFunction); | ||||
|                 }).then(() => { | ||||
|                     let timeSystem = openmct.time.timeSystem(); | ||||
|                     expect(timeSystem.key).toBe('local'); | ||||
|  | ||||
|                     openmct.time.off('timeSystem', resolveFunction); | ||||
|                 }); | ||||
|             }); | ||||
|             it("mode can be changed from realtime to fixed", () => { | ||||
|                 return switchToRealtimeMode().then(() => { | ||||
|                     expectLocationToBeInRealtimeMode(); | ||||
|  | ||||
|                     expect(openmct.time.clock()).toBeDefined(); | ||||
|                 }).then(switchToFixedMode).then(() => { | ||||
|                     let clock = openmct.time.clock(); | ||||
|                     expect(clock).not.toBeDefined(); | ||||
|                 }); | ||||
|             }); | ||||
|             it("bounds are correctly set in the API from the URL parameters", () => { | ||||
|                 let resolveFunction; | ||||
|  | ||||
|                 expectLocationToBeInFixedMode(); | ||||
|  | ||||
|                 return new Promise((resolve) => { | ||||
|                     resolveFunction = resolve; | ||||
|                     openmct.time.on('bounds', resolveFunction); | ||||
|                     let hash = window.location.hash; | ||||
|                     hash = hash.replace('tc.startBound=0', 'tc.startBound=222') | ||||
|                         .replace('tc.endBound=1', 'tc.endBound=333'); | ||||
|                     window.location.hash = hash; | ||||
|                 }).then(() => { | ||||
|                     let bounds = openmct.time.bounds(); | ||||
|  | ||||
|                     expect(bounds).toBeDefined(); | ||||
|                     expect(bounds.start).toBe(222); | ||||
|                     expect(bounds.end).toBe(333); | ||||
|                 }); | ||||
|             }); | ||||
|             it("bounds are correctly set in the API from the URL parameters where only the end bound changes", () => { | ||||
|                 let resolveFunction; | ||||
|  | ||||
|                 expectLocationToBeInFixedMode(); | ||||
|  | ||||
|                 return new Promise((resolve) => { | ||||
|                     resolveFunction = resolve; | ||||
|                     openmct.time.on('bounds', resolveFunction); | ||||
|                     let hash = window.location.hash; | ||||
|                     hash = hash.replace('tc.endBound=1', 'tc.endBound=333'); | ||||
|                     window.location.hash = hash; | ||||
|                 }).then(() => { | ||||
|                     let bounds = openmct.time.bounds(); | ||||
|  | ||||
|                     expect(bounds).toBeDefined(); | ||||
|                     expect(bounds.start).toBe(0); | ||||
|                     expect(bounds.end).toBe(333); | ||||
|                 }); | ||||
|             }); | ||||
|         }); | ||||
|         openmct.router.on('change:hash', resolveFunction); | ||||
|     }); | ||||
|  | ||||
|     function setRealtimeLocationParameters() { | ||||
|         let hash = window.location.hash.toString() | ||||
|             .replace('tc.mode=fixed', 'tc.mode=local') | ||||
|             .replace('tc.startBound=0', 'tc.startDelta=1000') | ||||
|             .replace('tc.endBound=1', 'tc.endDelta=100'); | ||||
|     it("when the clock mode is set to local, it is reflected in the URL", (done) => { | ||||
|         let success; | ||||
|  | ||||
|         window.location.hash = hash; | ||||
|     } | ||||
|         resolveFunction = () => { | ||||
|             let hash = window.location.hash; | ||||
|             hash = hash.replace('tc.mode=fixed', 'tc.mode=local'); | ||||
|             window.location.hash = hash; | ||||
|  | ||||
|     function setFixedLocationParameters() { | ||||
|         let hash = window.location.hash.toString() | ||||
|             .replace('tc.mode=local', 'tc.mode=fixed') | ||||
|             .replace('tc.timeSystem=utc', 'tc.timeSystem=local') | ||||
|             .replace('tc.startDelta=1000', 'tc.startBound=50') | ||||
|             .replace('tc.endDelta=100', 'tc.endBound=60'); | ||||
|             success = window.location.hash.includes('tc.mode=local'); | ||||
|             if (success) { | ||||
|                 expect(success).toBe(true); | ||||
|                 done(); | ||||
|             } | ||||
|         }; | ||||
|  | ||||
|         window.location.hash = hash; | ||||
|     } | ||||
|         openmct.router.on('change:hash', resolveFunction); | ||||
|     }); | ||||
|  | ||||
|     function switchToRealtimeMode() { | ||||
|         let resolveFunction; | ||||
|     it("when the clock mode is set to local, it is reflected in the URL", (done) => { | ||||
|         let success; | ||||
|  | ||||
|         return new Promise((resolve) => { | ||||
|             resolveFunction = resolve; | ||||
|             openmct.time.on('clock', resolveFunction); | ||||
|             setRealtimeLocationParameters(); | ||||
|         }).then(() => { | ||||
|             openmct.time.off('clock', resolveFunction); | ||||
|         }); | ||||
|     } | ||||
|         resolveFunction = () => { | ||||
|             let hash = window.location.hash; | ||||
|  | ||||
|     function switchToFixedMode() { | ||||
|         let resolveFunction; | ||||
|             hash = hash.replace('tc.mode=fixed', 'tc.mode=local'); | ||||
|             window.location.hash = hash; | ||||
|             success = window.location.hash.includes('tc.mode=local'); | ||||
|             if (success) { | ||||
|                 expect(success).toBe(true); | ||||
|                 done(); | ||||
|             } | ||||
|         }; | ||||
|  | ||||
|         return new Promise((resolve) => { | ||||
|             resolveFunction = resolve; | ||||
|             //The 'hashchange' event appears to be asynchronous, so we need to wait until a clock change has been | ||||
|             //detected in the API. | ||||
|             openmct.time.on('clock', resolveFunction); | ||||
|             setFixedLocationParameters(); | ||||
|         }).then(() => { | ||||
|             openmct.time.off('clock', resolveFunction); | ||||
|         }); | ||||
|     } | ||||
|         openmct.router.on('change:hash', resolveFunction); | ||||
|     }); | ||||
|  | ||||
|     function expectLocationToBeInRealtimeMode() { | ||||
|         expect(window.location.hash.includes('tc.mode=local')).toBe(true); | ||||
|         expect(window.location.hash.includes('tc.startDelta=1000')).toBe(true); | ||||
|         expect(window.location.hash.includes('tc.endDelta=100')).toBe(true); | ||||
|         expect(window.location.hash.includes('tc.mode=fixed')).toBe(false); | ||||
|     } | ||||
|     it("reset hash", (done) => { | ||||
|         let success; | ||||
|  | ||||
|     function expectLocationToBeInFixedMode() { | ||||
|         expect(window.location.hash.includes('tc.mode=fixed')).toBe(true); | ||||
|         expect(window.location.hash.includes('tc.startBound=0')).toBe(true); | ||||
|         expect(window.location.hash.includes('tc.endBound=1')).toBe(true); | ||||
|         expect(window.location.hash.includes('tc.mode=local')).toBe(false); | ||||
|     } | ||||
|         window.location.hash = oldHash; | ||||
|         resolveFunction = () => { | ||||
|             success = window.location.hash === oldHash; | ||||
|             if (success) { | ||||
|                 expect(success).toBe(true); | ||||
|                 done(); | ||||
|             } | ||||
|         }; | ||||
|  | ||||
|         openmct.router.on('change:hash', resolveFunction); | ||||
|     }); | ||||
| }); | ||||
|   | ||||
| @@ -43,7 +43,6 @@ import {TRIGGER_CONJUNCTION, TRIGGER_LABEL} from "./utils/constants"; | ||||
| * } | ||||
| */ | ||||
| export default class Condition extends EventEmitter { | ||||
|  | ||||
|     /** | ||||
|      * Manages criteria and emits the result of - true or false - based on criteria evaluated. | ||||
|      * @constructor | ||||
|   | ||||
| @@ -52,7 +52,6 @@ | ||||
|         <div class="c-inspect-styles__content c-inspect-styles__condition-set"> | ||||
|             <a v-if="conditionSetDomainObject" | ||||
|                class="c-object-label icon-conditional" | ||||
|                :href="navigateToPath" | ||||
|                @click="navigateOrPreview" | ||||
|             > | ||||
|                 <span class="c-object-label__name">{{ conditionSetDomainObject.name }}</span> | ||||
| @@ -286,6 +285,8 @@ export default { | ||||
|             if (this.openmct.editor.isEditing()) { | ||||
|                 event.preventDefault(); | ||||
|                 this.previewAction.invoke(this.objectPath); | ||||
|             } else { | ||||
|                 this.openmct.router.navigate(this.navigateToPath); | ||||
|             } | ||||
|         }, | ||||
|         removeConditionSet() { | ||||
|   | ||||
| @@ -66,7 +66,6 @@ | ||||
|         <div class="c-inspect-styles__content c-inspect-styles__condition-set"> | ||||
|             <a v-if="conditionSetDomainObject" | ||||
|                class="c-object-label" | ||||
|                :href="navigateToPath" | ||||
|                @click="navigateOrPreview" | ||||
|             > | ||||
|                 <span class="c-object-label__type-icon icon-conditional"></span> | ||||
| @@ -309,6 +308,8 @@ export default { | ||||
|             if (this.openmct.editor.isEditing()) { | ||||
|                 event.preventDefault(); | ||||
|                 this.previewAction.invoke(this.objectPath); | ||||
|             } else { | ||||
|                 this.openmct.router.navigate(this.navigateToPath); | ||||
|             } | ||||
|         }, | ||||
|         isItemType(type, item) { | ||||
|   | ||||
| @@ -46,7 +46,7 @@ xdescribe("the plugin", () => { | ||||
|     }); | ||||
|  | ||||
|     afterEach(() => { | ||||
|         resetApplicationState(openmct); | ||||
|         return resetApplicationState(openmct); | ||||
|     }); | ||||
|  | ||||
|     it('installs the new folder action', () => { | ||||
|   | ||||
| @@ -235,7 +235,7 @@ define(['lodash'], function (_) { | ||||
|                                 message: `Warning! This action will remove this item from the Display Layout. Do you want to continue?`, | ||||
|                                 buttons: [ | ||||
|                                     { | ||||
|                                         label: 'Ok', | ||||
|                                         label: 'OK', | ||||
|                                         emphasis: 'true', | ||||
|                                         callback: function () { | ||||
|                                             removeItem(getAllTypes(selection)); | ||||
|   | ||||
| @@ -112,7 +112,7 @@ describe("The Duplicate Action plugin", () => { | ||||
|     }); | ||||
|  | ||||
|     afterEach(() => { | ||||
|         resetApplicationState(openmct); | ||||
|         return resetApplicationState(openmct); | ||||
|     }); | ||||
|  | ||||
|     it("should be defined", () => { | ||||
|   | ||||
| @@ -97,7 +97,7 @@ function ToolbarProvider(openmct) { | ||||
|                             message: `This action will remove this frame from this Flexible Layout. Do you want to continue?`, | ||||
|                             buttons: [ | ||||
|                                 { | ||||
|                                     label: 'Ok', | ||||
|                                     label: 'OK', | ||||
|                                     emphasis: 'true', | ||||
|                                     callback: function () { | ||||
|                                         deleteFrameAction(primary.context.frameId); | ||||
| @@ -162,7 +162,7 @@ function ToolbarProvider(openmct) { | ||||
|                             message: 'This action will permanently delete this container from this Flexible Layout', | ||||
|                             buttons: [ | ||||
|                                 { | ||||
|                                     label: 'Ok', | ||||
|                                     label: 'OK', | ||||
|                                     emphasis: 'true', | ||||
|                                     callback: function () { | ||||
|                                         removeContainer(containerId); | ||||
|   | ||||
| @@ -5,7 +5,7 @@ | ||||
|         'is-alias': item.isAlias === true, | ||||
|         'c-grid-item--unknown': item.type.cssClass === undefined || item.type.cssClass.indexOf('unknown') !== -1 | ||||
|     }, statusClass]" | ||||
|     :href="objectLink" | ||||
|     @click="navigate" | ||||
| > | ||||
|     <div | ||||
|         class="c-grid-item__type-icon" | ||||
| @@ -49,11 +49,17 @@ import statusListener from './status-listener'; | ||||
|  | ||||
| export default { | ||||
|     mixins: [contextMenuGesture, objectLink, statusListener], | ||||
|     inject: ['openmct'], | ||||
|     props: { | ||||
|         item: { | ||||
|             type: Object, | ||||
|             required: true | ||||
|         } | ||||
|     }, | ||||
|     methods: { | ||||
|         navigate() { | ||||
|             this.openmct.router.navigate(this.objectLink); | ||||
|         } | ||||
|     } | ||||
| }; | ||||
| </script> | ||||
|   | ||||
| @@ -11,7 +11,7 @@ | ||||
|             ref="objectLink" | ||||
|             class="c-object-label" | ||||
|             :class="[statusClass]" | ||||
|             :href="objectLink" | ||||
|             @click="navigate" | ||||
|         > | ||||
|             <div | ||||
|                 class="c-object-label__type-icon c-list-item__name__type-icon" | ||||
| @@ -45,6 +45,7 @@ import statusListener from './status-listener'; | ||||
|  | ||||
| export default { | ||||
|     mixins: [contextMenuGesture, objectLink, statusListener], | ||||
|     inject: ['openmct'], | ||||
|     props: { | ||||
|         item: { | ||||
|             type: Object, | ||||
| @@ -56,7 +57,7 @@ export default { | ||||
|             return moment(timestamp).format(format); | ||||
|         }, | ||||
|         navigate() { | ||||
|             this.$refs.objectLink.click(); | ||||
|             this.openmct.router.navigate(this.objectLink); | ||||
|         } | ||||
|     } | ||||
| }; | ||||
|   | ||||
| @@ -41,7 +41,7 @@ export default class GoToOriginalAction { | ||||
|                     .slice(1) | ||||
|                     .join('/'); | ||||
|  | ||||
|                 window.location.href = url; | ||||
|                 this._openmct.router.navigate(url); | ||||
|             }); | ||||
|     } | ||||
|     appliesTo(objectPath) { | ||||
|   | ||||
| @@ -47,7 +47,6 @@ describe("the plugin", () => { | ||||
|     }); | ||||
|  | ||||
|     describe('when invoked', () => { | ||||
|  | ||||
|         beforeEach(() => { | ||||
|             mockObjectPath = [{ | ||||
|                 name: 'mock folder', | ||||
| @@ -63,11 +62,15 @@ describe("the plugin", () => { | ||||
|                     key: 'test' | ||||
|                 } | ||||
|             })); | ||||
|  | ||||
|             goToFolderAction.invoke(mockObjectPath); | ||||
|         }); | ||||
|  | ||||
|         it('goes to the original location', () => { | ||||
|             expect(window.location.href).toContain('context.html#/browse/?tc.mode=fixed&tc.startBound=0&tc.endBound=1&tc.timeSystem=utc'); | ||||
|         it('goes to the original location', (done) => { | ||||
|             setTimeout(() => { | ||||
|                 expect(window.location.href).toContain('context.html#/browse/?tc.mode=fixed&tc.startBound=0&tc.endBound=1&tc.timeSystem=utc'); | ||||
|                 done(); | ||||
|             }, 2500); | ||||
|         }); | ||||
|     }); | ||||
| }); | ||||
|   | ||||
| @@ -390,7 +390,9 @@ export default { | ||||
|             delete this.unsubscribe; | ||||
|         } | ||||
|  | ||||
|         this.imageContainerResizeObserver.disconnect(); | ||||
|         if (this.imageContainerResizeObserver) { | ||||
|             this.imageContainerResizeObserver.disconnect(); | ||||
|         } | ||||
|  | ||||
|         if (this.relatedTelemetry.hasRelatedTelemetry) { | ||||
|             this.relatedTelemetry.destroy(); | ||||
| @@ -702,7 +704,7 @@ export default { | ||||
|             window.clearInterval(this.durationTracker); | ||||
|         }, | ||||
|         updateDuration() { | ||||
|             let currentTime = this.openmct.time.clock().currentValue(); | ||||
|             let currentTime = this.openmct.time.clock() && this.openmct.time.clock().currentValue(); | ||||
|             this.numericDuration = currentTime - this.parsedSelectedTime; | ||||
|         }, | ||||
|         resetAgeCSS() { | ||||
|   | ||||
| @@ -19,7 +19,7 @@ | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
| import ImageryPlugin from './plugin.js'; | ||||
|  | ||||
| import Vue from 'vue'; | ||||
| import { | ||||
|     createOpenMct, | ||||
| @@ -89,15 +89,11 @@ describe("The Imagery View Layout", () => { | ||||
|     const START = Date.now(); | ||||
|     const COUNT = 10; | ||||
|  | ||||
|     let resolveFunction; | ||||
|  | ||||
|     let openmct; | ||||
|     let imageryPlugin; | ||||
|     let parent; | ||||
|     let child; | ||||
|     let timeFormat = 'utc'; | ||||
|     let bounds = { | ||||
|         start: START - TEN_MINUTES, | ||||
|         end: START | ||||
|     }; | ||||
|     let imageTelemetry = generateTelemetry(START - TEN_MINUTES, COUNT); | ||||
|     let imageryObject = { | ||||
|         identifier: { | ||||
| @@ -205,6 +201,10 @@ describe("The Imagery View Layout", () => { | ||||
|  | ||||
|         openmct = createOpenMct(); | ||||
|  | ||||
|         openmct.install(openmct.plugins.MyItems()); | ||||
|         openmct.install(openmct.plugins.LocalTimeSystem()); | ||||
|         openmct.install(openmct.plugins.UTCTimeSystem()); | ||||
|  | ||||
|         parent = document.createElement('div'); | ||||
|         child = document.createElement('div'); | ||||
|         parent.appendChild(child); | ||||
| @@ -215,22 +215,18 @@ describe("The Imagery View Layout", () => { | ||||
|         }); | ||||
|  | ||||
|         spyOn(openmct.telemetry, 'request').and.returnValue(Promise.resolve([])); | ||||
|  | ||||
|         imageryPlugin = new ImageryPlugin(); | ||||
|         openmct.install(imageryPlugin); | ||||
|  | ||||
|         spyOn(openmct.objects, 'get').and.returnValue(Promise.resolve({})); | ||||
|  | ||||
|         openmct.time.timeSystem(timeFormat, { | ||||
|             start: 0, | ||||
|             end: 4 | ||||
|         }); | ||||
|  | ||||
|         openmct.on('start', done); | ||||
|         openmct.startHeadless(appHolder); | ||||
|         openmct.start(appHolder); | ||||
|     }); | ||||
|  | ||||
|     afterEach(() => { | ||||
|         openmct.time.timeSystem('utc', { | ||||
|             start: 0, | ||||
|             end: 1 | ||||
|         }); | ||||
|  | ||||
|         return resetApplicationState(openmct); | ||||
|     }); | ||||
|  | ||||
| @@ -248,7 +244,7 @@ describe("The Imagery View Layout", () => { | ||||
|         let imageryViewProvider; | ||||
|         let imageryView; | ||||
|  | ||||
|         beforeEach(async (done) => { | ||||
|         beforeEach(async () => { | ||||
|             let telemetryRequestResolve; | ||||
|             let telemetryRequestPromise = new Promise((resolve) => { | ||||
|                 telemetryRequestResolve = resolve; | ||||
| @@ -260,23 +256,18 @@ describe("The Imagery View Layout", () => { | ||||
|                 return telemetryRequestPromise; | ||||
|             }); | ||||
|  | ||||
|             openmct.time.clock('local', { | ||||
|                 start: bounds.start, | ||||
|                 end: bounds.end + 100 | ||||
|             }); | ||||
|  | ||||
|             applicableViews = openmct.objectViews.get(imageryObject, []); | ||||
|             imageryViewProvider = applicableViews.find(viewProvider => viewProvider.key === imageryKey); | ||||
|             imageryView = imageryViewProvider.view(imageryObject); | ||||
|             imageryView.show(child); | ||||
|  | ||||
|             await telemetryRequestPromise; | ||||
|             await Vue.nextTick(); | ||||
|  | ||||
|             return done(); | ||||
|         }); | ||||
|  | ||||
|         afterEach(() => { | ||||
|             openmct.time.stopClock(); | ||||
|             openmct.router.removeListener('change:hash', resolveFunction); | ||||
|  | ||||
|             imageryView.destroy(); | ||||
|         }); | ||||
|  | ||||
| @@ -286,43 +277,44 @@ describe("The Imagery View Layout", () => { | ||||
|             expect(imageInfo.url.indexOf(imageTelemetry[COUNT - 1].timeId)).not.toEqual(-1); | ||||
|         }); | ||||
|  | ||||
|         it("should show the clicked thumbnail as the main image", async () => { | ||||
|         it("should show the clicked thumbnail as the main image", (done) => { | ||||
|             const target = imageTelemetry[5].url; | ||||
|             parent.querySelectorAll(`img[src='${target}']`)[0].click(); | ||||
|             await Vue.nextTick(); | ||||
|             const imageInfo = getImageInfo(parent); | ||||
|             Vue.nextTick(() => { | ||||
|                 const imageInfo = getImageInfo(parent); | ||||
|  | ||||
|             expect(imageInfo.url.indexOf(imageTelemetry[5].timeId)).not.toEqual(-1); | ||||
|         }); | ||||
|  | ||||
|         it("should show that an image is new", async (done) => { | ||||
|             await Vue.nextTick(); | ||||
|  | ||||
|             // used in code, need to wait to the 500ms here too | ||||
|             setTimeout(() => { | ||||
|                 const imageIsNew = isNew(parent); | ||||
|  | ||||
|                 expect(imageIsNew).toBeTrue(); | ||||
|                 expect(imageInfo.url.indexOf(imageTelemetry[5].timeId)).not.toEqual(-1); | ||||
|                 done(); | ||||
|             }, REFRESH_CSS_MS); | ||||
|             }); | ||||
|         }); | ||||
|  | ||||
|         it("should show that an image is not new", async (done) => { | ||||
|         xit("should show that an image is new", (done) => { | ||||
|             Vue.nextTick(() => { | ||||
|                 // used in code, need to wait to the 500ms here too | ||||
|                 setTimeout(() => { | ||||
|                     const imageIsNew = isNew(parent); | ||||
|                     expect(imageIsNew).toBeTrue(); | ||||
|                     done(); | ||||
|                 }, REFRESH_CSS_MS); | ||||
|             }); | ||||
|         }); | ||||
|  | ||||
|         xit("should show that an image is not new", (done) => { | ||||
|             const target = imageTelemetry[2].url; | ||||
|             parent.querySelectorAll(`img[src='${target}']`)[0].click(); | ||||
|  | ||||
|             await Vue.nextTick(); | ||||
|             Vue.nextTick(() => { | ||||
|                 // used in code, need to wait to the 500ms here too | ||||
|                 setTimeout(() => { | ||||
|                     const imageIsNew = isNew(parent); | ||||
|  | ||||
|             // used in code, need to wait to the 500ms here too | ||||
|             setTimeout(() => { | ||||
|                 const imageIsNew = isNew(parent); | ||||
|  | ||||
|                 expect(imageIsNew).toBeFalse(); | ||||
|                 done(); | ||||
|             }, REFRESH_CSS_MS); | ||||
|                     expect(imageIsNew).toBeFalse(); | ||||
|                     done(); | ||||
|                 }, REFRESH_CSS_MS); | ||||
|             }); | ||||
|         }); | ||||
|  | ||||
|         it("should navigate via arrow keys", async () => { | ||||
|         it("should navigate via arrow keys", (done) => { | ||||
|             let keyOpts = { | ||||
|                 element: parent.querySelector('.c-imagery'), | ||||
|                 key: 'ArrowLeft', | ||||
| @@ -332,14 +324,15 @@ describe("The Imagery View Layout", () => { | ||||
|  | ||||
|             simulateKeyEvent(keyOpts); | ||||
|  | ||||
|             await Vue.nextTick(); | ||||
|             Vue.nextTick(() => { | ||||
|                 const imageInfo = getImageInfo(parent); | ||||
|  | ||||
|             const imageInfo = getImageInfo(parent); | ||||
|  | ||||
|             expect(imageInfo.url.indexOf(imageTelemetry[COUNT - 2].timeId)).not.toEqual(-1); | ||||
|                 expect(imageInfo.url.indexOf(imageTelemetry[COUNT - 2].timeId)).not.toEqual(-1); | ||||
|                 done(); | ||||
|             }); | ||||
|         }); | ||||
|  | ||||
|         it("should navigate via numerous arrow keys", async () => { | ||||
|         it("should navigate via numerous arrow keys", (done) => { | ||||
|             let element = parent.querySelector('.c-imagery'); | ||||
|             let type = 'keyup'; | ||||
|             let leftKeyOpts = { | ||||
| @@ -362,12 +355,12 @@ describe("The Imagery View Layout", () => { | ||||
|             // right once | ||||
|             simulateKeyEvent(rightKeyOpts); | ||||
|  | ||||
|             await Vue.nextTick(); | ||||
|             Vue.nextTick(() => { | ||||
|                 const imageInfo = getImageInfo(parent); | ||||
|  | ||||
|             const imageInfo = getImageInfo(parent); | ||||
|  | ||||
|             expect(imageInfo.url.indexOf(imageTelemetry[COUNT - 3].timeId)).not.toEqual(-1); | ||||
|                 expect(imageInfo.url.indexOf(imageTelemetry[COUNT - 3].timeId)).not.toEqual(-1); | ||||
|                 done(); | ||||
|             }); | ||||
|         }); | ||||
|  | ||||
|     }); | ||||
| }); | ||||
|   | ||||
| @@ -55,7 +55,7 @@ describe("The local time", () => { | ||||
|         beforeEach(() => { | ||||
|             localTimeSystem = openmct.time.timeSystem(LOCAL_SYSTEM_KEY, { | ||||
|                 start: 0, | ||||
|                 end: 4 | ||||
|                 end: 1 | ||||
|             }); | ||||
|         }); | ||||
|  | ||||
|   | ||||
| @@ -81,7 +81,7 @@ describe("The Move Action plugin", () => { | ||||
|     }); | ||||
|  | ||||
|     afterEach(() => { | ||||
|         resetApplicationState(openmct); | ||||
|         return resetApplicationState(openmct); | ||||
|     }); | ||||
|  | ||||
|     it("should be defined", () => { | ||||
|   | ||||
| @@ -135,6 +135,7 @@ import SearchResults from './SearchResults.vue'; | ||||
| import Sidebar from './Sidebar.vue'; | ||||
| import { clearDefaultNotebook, getDefaultNotebook, setDefaultNotebook, setDefaultNotebookSection, setDefaultNotebookPage } from '../utils/notebook-storage'; | ||||
| import { addNotebookEntry, createNewEmbed, getEntryPosById, getNotebookEntries, mutateObject } from '../utils/notebook-entries'; | ||||
| import { NOTEBOOK_VIEW_TYPE } from '../notebook-constants'; | ||||
| import objectUtils from 'objectUtils'; | ||||
|  | ||||
| import { debounce } from 'lodash'; | ||||
| @@ -189,14 +190,14 @@ export default { | ||||
|         selectedPage() { | ||||
|             const pages = this.getPages(); | ||||
|             if (!pages) { | ||||
|                 return null; | ||||
|                 return {}; | ||||
|             } | ||||
|  | ||||
|             return pages.find(page => page.isSelected); | ||||
|         }, | ||||
|         selectedSection() { | ||||
|             if (!this.sections.length) { | ||||
|                 return null; | ||||
|                 return {}; | ||||
|             } | ||||
|  | ||||
|             return this.sections.find(section => section.isSelected); | ||||
| @@ -216,6 +217,7 @@ export default { | ||||
|  | ||||
|         window.addEventListener('orientationchange', this.formatSidebar); | ||||
|         window.addEventListener("hashchange", this.navigateToSectionPage, false); | ||||
|         this.openmct.router.on('change:params', this.changeSectionPage); | ||||
|  | ||||
|         this.navigateToSectionPage(); | ||||
|     }, | ||||
| @@ -226,6 +228,7 @@ export default { | ||||
|  | ||||
|         window.removeEventListener('orientationchange', this.formatSidebar); | ||||
|         window.removeEventListener("hashchange", this.navigateToSectionPage); | ||||
|         this.openmct.router.off('change:params', this.changeSectionPage); | ||||
|     }, | ||||
|     updated: function () { | ||||
|         this.$nextTick(() => { | ||||
| @@ -233,6 +236,28 @@ export default { | ||||
|         }); | ||||
|     }, | ||||
|     methods: { | ||||
|         changeSectionPage(newParams, oldParams, changedParams) { | ||||
|             if (newParams.view !== NOTEBOOK_VIEW_TYPE) { | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             let pageId = newParams.pageId; | ||||
|             let sectionId = newParams.sectionId; | ||||
|  | ||||
|             if (!pageId && !sectionId) { | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             this.sections.forEach(section => { | ||||
|                 section.isSelected = Boolean(section.id === sectionId); | ||||
|  | ||||
|                 if (section.isSelected) { | ||||
|                     section.pages.forEach(page => { | ||||
|                         page.isSelected = Boolean(page.id === pageId); | ||||
|                     }); | ||||
|                 } | ||||
|             }); | ||||
|         }, | ||||
|         changeSelectedSection({ sectionId, pageId }) { | ||||
|             const sections = this.sections.map(s => { | ||||
|                 s.isSelected = false; | ||||
| @@ -518,9 +543,11 @@ export default { | ||||
|             return this.sections.find(section => section.isSelected); | ||||
|         }, | ||||
|         navigateToSectionPage() { | ||||
|             const { pageId, sectionId } = this.openmct.router.getParams(); | ||||
|             let { pageId, sectionId } = this.openmct.router.getParams(); | ||||
|  | ||||
|             if (!pageId || !sectionId) { | ||||
|                 return; | ||||
|                 sectionId = this.selectedSection.id; | ||||
|                 pageId = this.selectedPage.id; | ||||
|             } | ||||
|  | ||||
|             const sections = this.sections.map(s => { | ||||
|   | ||||
| @@ -145,7 +145,7 @@ export default { | ||||
|  | ||||
|             const relativeHash = hash.slice(hash.indexOf('#')); | ||||
|             const url = new URL(relativeHash, `${location.protocol}//${location.host}${location.pathname}`); | ||||
|             window.location.hash = url.hash; | ||||
|             this.openmct.router.navigate(url.hash); | ||||
|         }, | ||||
|         formatTime(unixTime, timeFormat) { | ||||
|             return Moment.utc(unixTime).format(timeFormat); | ||||
|   | ||||
| @@ -111,10 +111,6 @@ export default { | ||||
|             } | ||||
|         } | ||||
|     }, | ||||
|     data() { | ||||
|         return { | ||||
|         }; | ||||
|     }, | ||||
|     computed: { | ||||
|         pages() { | ||||
|             const selectedSection = this.sections.find(section => section.isSelected); | ||||
|   | ||||
| @@ -1,3 +1,4 @@ | ||||
| export const EVENT_SNAPSHOTS_UPDATED = 'SNAPSHOTS_UPDATED'; | ||||
| export const NOTEBOOK_DEFAULT = 'DEFAULT'; | ||||
| export const NOTEBOOK_SNAPSHOT = 'SNAPSHOT'; | ||||
| export const NOTEBOOK_VIEW_TYPE = 'notebook-vue'; | ||||
|   | ||||
| @@ -65,7 +65,8 @@ describe("Notebook plugin:", () => { | ||||
|  | ||||
|     afterAll(() => { | ||||
|         appHolder.remove(); | ||||
|         resetApplicationState(openmct); | ||||
|  | ||||
|         return resetApplicationState(openmct); | ||||
|     }); | ||||
|  | ||||
|     it("has type as Notebook", () => { | ||||
|   | ||||
| @@ -140,7 +140,8 @@ describe('Notebook Entries:', () => { | ||||
|  | ||||
|     afterEach(() => { | ||||
|         notebookDomainObject.configuration.entries[selectedSection.id][selectedPage.id] = []; | ||||
|         resetApplicationState(openmct); | ||||
|  | ||||
|         return resetApplicationState(openmct); | ||||
|     }); | ||||
|  | ||||
|     it('getNotebookEntries has no entries', () => { | ||||
|   | ||||
| @@ -83,7 +83,7 @@ describe('Notebook Storage:', () => { | ||||
|     }); | ||||
|  | ||||
|     afterEach(() => { | ||||
|         resetApplicationState(openmct); | ||||
|         return resetApplicationState(openmct); | ||||
|     }); | ||||
|  | ||||
|     it('has empty local Storage', () => { | ||||
|   | ||||
| @@ -117,7 +117,7 @@ describe('the plugin', () => { | ||||
|             }); | ||||
|         }); | ||||
|  | ||||
|         it('updates an object', () => { | ||||
|         it('updates an object', (done) => { | ||||
|             return openmct.objects.save(mockDomainObject).then((result) => { | ||||
|                 expect(result).toBeTrue(); | ||||
|                 expect(provider.create).toHaveBeenCalled(); | ||||
| @@ -128,6 +128,7 @@ describe('the plugin', () => { | ||||
|                 return openmct.objects.save(mockDomainObject).then((updatedResult) => { | ||||
|                     expect(updatedResult).toBeTrue(); | ||||
|                     expect(provider.update).toHaveBeenCalled(); | ||||
|                     done(); | ||||
|                 }); | ||||
|             }); | ||||
|         }); | ||||
|   | ||||
| @@ -159,6 +159,7 @@ import MctTicks from "./MctTicks.vue"; | ||||
| import MctChart from "./chart/MctChart.vue"; | ||||
| import XAxis from "./axis/XAxis.vue"; | ||||
| import YAxis from "./axis/YAxis.vue"; | ||||
| import _ from "lodash"; | ||||
|  | ||||
| export default { | ||||
|     components: { | ||||
| @@ -496,6 +497,10 @@ export default { | ||||
|         }, | ||||
|  | ||||
|         initialize() { | ||||
|             _.debounce(this.handleWindowResize, 400); | ||||
|             this.plotContainerResizeObserver = new ResizeObserver(this.handleWindowResize); | ||||
|             this.plotContainerResizeObserver.observe(this.$parent.$refs.plotWrapper); | ||||
|  | ||||
|             // Setup canvas etc. | ||||
|             this.xScale = new LinearScale(this.config.xAxis.get('displayRange')); | ||||
|             this.yScale = new LinearScale(this.config.yAxis.get('displayRange')); | ||||
| @@ -999,12 +1004,20 @@ export default { | ||||
|                 this.removeStatusListener(); | ||||
|             } | ||||
|  | ||||
|             this.plotContainerResizeObserver.disconnect(); | ||||
|  | ||||
|             this.openmct.time.off('clock', this.updateRealTime); | ||||
|             this.openmct.time.off('bounds', this.updateDisplayBounds); | ||||
|             this.openmct.objectViews.off('clearData', this.clearData); | ||||
|         }, | ||||
|         updateStatus(status) { | ||||
|             this.$emit('statusUpdated', status); | ||||
|         }, | ||||
|         handleWindowResize() { | ||||
|             if (this.offsetWidth !== this.$parent.$refs.plotWrapper.offsetWidth) { | ||||
|                 this.offsetWidth = this.$parent.$refs.plotWrapper.offsetWidth; | ||||
|                 this.config.series.models.forEach(this.loadSeriesData, this); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| }; | ||||
|   | ||||
| @@ -228,15 +228,16 @@ export default { | ||||
|  | ||||
|         doTickUpdate() { | ||||
|             if (this.shouldCheckWidth) { | ||||
|                 const tickElements = this.$refs.tickContainer.querySelectorAll('.gl-plot-tick > span'); | ||||
|                 const tickElements = this.$refs.tickContainer && this.$refs.tickContainer.querySelectorAll('.gl-plot-tick > span'); | ||||
|                 if (tickElements) { | ||||
|                     const tickWidth = Number([].reduce.call(tickElements, function (memo, first) { | ||||
|                         return Math.max(memo, first.offsetWidth); | ||||
|                     }, 0)); | ||||
|  | ||||
|                 const tickWidth = Number([].reduce.call(tickElements, function (memo, first) { | ||||
|                     return Math.max(memo, first.offsetWidth); | ||||
|                 }, 0)); | ||||
|  | ||||
|                 this.tickWidth = tickWidth; | ||||
|                 this.$emit('plotTickWidth', tickWidth); | ||||
|                 this.shouldCheckWidth = false; | ||||
|                     this.tickWidth = tickWidth; | ||||
|                     this.$emit('plotTickWidth', tickWidth); | ||||
|                     this.shouldCheckWidth = false; | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             this.tickUpdate = false; | ||||
|   | ||||
| @@ -99,6 +99,11 @@ describe("the plugin", function () { | ||||
|         element.appendChild(child); | ||||
|         document.body.appendChild(element); | ||||
|  | ||||
|         spyOn(window, 'ResizeObserver').and.returnValue({ | ||||
|             observe() {}, | ||||
|             disconnect() {} | ||||
|         }); | ||||
|  | ||||
|         openmct.time.timeSystem("utc", { | ||||
|             start: 0, | ||||
|             end: 4 | ||||
| @@ -118,6 +123,11 @@ describe("the plugin", function () { | ||||
|     }); | ||||
|  | ||||
|     afterEach((done) => { | ||||
|         openmct.time.timeSystem('utc', { | ||||
|             start: 0, | ||||
|             end: 1 | ||||
|         }); | ||||
|  | ||||
|         // Needs to be in a timeout because plots use a bunch of setTimeouts, some of which can resolve during or after | ||||
|         // teardown, which causes problems | ||||
|         // This is hacky, we should find a better approach here. | ||||
| @@ -129,7 +139,7 @@ describe("the plugin", function () { | ||||
|  | ||||
|             configStore.deleteAll(); | ||||
|  | ||||
|             resetApplicationState(openmct).then(done); | ||||
|             resetApplicationState(openmct).then(done).catch(done); | ||||
|         }); | ||||
|     }); | ||||
|  | ||||
|   | ||||
| @@ -78,7 +78,7 @@ export default class RemoveAction { | ||||
|             .map(object => this.openmct.objects.makeKeyString(object.identifier)) | ||||
|             .join("/"); | ||||
|  | ||||
|         window.location.href = '#/browse/' + urlPath; | ||||
|         this.openmct.router.navigate('#/browse/' + urlPath); | ||||
|     } | ||||
|  | ||||
|     removeFromComposition(parent, child) { | ||||
|   | ||||
| @@ -72,7 +72,7 @@ describe("The Remove Action plugin", () => { | ||||
|     }); | ||||
|  | ||||
|     afterEach(() => { | ||||
|         resetApplicationState(openmct); | ||||
|         return resetApplicationState(openmct); | ||||
|     }); | ||||
|  | ||||
|     it("should be defined", () => { | ||||
|   | ||||
| @@ -65,11 +65,6 @@ | ||||
| <script> | ||||
| import ObjectView from '../../../ui/components/ObjectView.vue'; | ||||
| import RemoveAction from '../../remove/RemoveAction.js'; | ||||
| import { | ||||
|     getSearchParam, | ||||
|     setSearchParam, | ||||
|     deleteSearchParam | ||||
| } from 'utils/openmctLocation'; | ||||
|  | ||||
| const unknownObjectType = { | ||||
|     definition: { | ||||
| @@ -115,7 +110,7 @@ export default { | ||||
|             this.composition.on('remove', this.removeItem); | ||||
|             this.composition.on('reorder', this.onReorder); | ||||
|             this.composition.load().then(() => { | ||||
|                 let currentTabIndexFromURL = getSearchParam(this.searchTabKey); | ||||
|                 let currentTabIndexFromURL = this.openmct.router.getSearchParam(this.searchTabKey); | ||||
|                 let currentTabIndexFromDomainObject = this.internalDomainObject.currentTabIndex; | ||||
|  | ||||
|                 if (currentTabIndexFromURL !== null) { | ||||
| @@ -129,6 +124,8 @@ export default { | ||||
|  | ||||
|         this.unsubscribe = this.openmct.objects.observe(this.internalDomainObject, '*', this.updateInternalDomainObject); | ||||
|  | ||||
|         this.openmct.router.on('change:params', this.updateCurrentTab.bind(this)); | ||||
|  | ||||
|         this.RemoveAction = new RemoveAction(this.openmct); | ||||
|         document.addEventListener('dragstart', this.dragstart); | ||||
|         document.addEventListener('dragend', this.dragend); | ||||
| @@ -148,6 +145,8 @@ export default { | ||||
|         this.unsubscribe(); | ||||
|         this.clearCurrentTabIndexFromURL(); | ||||
|  | ||||
|         this.openmct.router.off('change:params', this.updateCurrentTab.bind(this)); | ||||
|  | ||||
|         document.removeEventListener('dragstart', this.dragstart); | ||||
|         document.removeEventListener('dragend', this.dragend); | ||||
|     }, | ||||
| @@ -181,7 +180,7 @@ export default { | ||||
|                 message: `This action will remove this tab from the Tabs Layout. Do you want to continue?`, | ||||
|                 buttons: [ | ||||
|                     { | ||||
|                         label: 'Ok', | ||||
|                         label: 'OK', | ||||
|                         emphasis: 'true', | ||||
|                         callback: () => { | ||||
|                             this.composition.remove(childDomainObject); | ||||
| @@ -285,15 +284,15 @@ export default { | ||||
|             this.openmct.objects.mutate(this.internalDomainObject, 'currentTabIndex', index); | ||||
|         }, | ||||
|         storeCurrentTabIndexInURL(index) { | ||||
|             let currentTabIndexInURL = getSearchParam(this.searchTabKey); | ||||
|             let currentTabIndexInURL = this.openmct.router.getSearchParam(this.searchTabKey); | ||||
|  | ||||
|             if (index !== currentTabIndexInURL) { | ||||
|                 setSearchParam(this.searchTabKey, index); | ||||
|                 this.openmct.router.setSearchParam(this.searchTabKey, index); | ||||
|                 this.currentTabIndex = index; | ||||
|             } | ||||
|         }, | ||||
|         clearCurrentTabIndexFromURL() { | ||||
|             deleteSearchParam(this.searchTabKey); | ||||
|             this.openmct.router.deleteSearchParam(this.searchTabKey); | ||||
|         }, | ||||
|         updateStatus(keyString, status) { | ||||
|             let tabPos = this.tabsList.findIndex((tab) => { | ||||
| @@ -309,6 +308,19 @@ export default { | ||||
|             } else { | ||||
|                 return this.loadedTabs[tab.keyString]; | ||||
|             } | ||||
|         }, | ||||
|         updateCurrentTab(newParams, oldParams, changedParams) { | ||||
|             const tabIndex = changedParams[this.searchTabKey]; | ||||
|             if (!tabIndex) { | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             if (this.currentTabIndex === parseInt(tabIndex, 10)) { | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             this.currentTabIndex = tabIndex; | ||||
|             this.currentTab = this.tabsList[tabIndex]; | ||||
|         } | ||||
|     } | ||||
| }; | ||||
|   | ||||
| @@ -82,6 +82,11 @@ describe("the plugin", () => { | ||||
|     }); | ||||
|  | ||||
|     afterEach(() => { | ||||
|         openmct.time.timeSystem('utc', { | ||||
|             start: 0, | ||||
|             end: 1 | ||||
|         }); | ||||
|  | ||||
|         return resetApplicationState(openmct); | ||||
|     }); | ||||
|  | ||||
|   | ||||
| @@ -59,7 +59,7 @@ describe("The UTC Time System", () => { | ||||
|         it("can be set to be the main time system", () => { | ||||
|             openmct.time.timeSystem(UTC_SYSTEM_AND_FORMAT_KEY, { | ||||
|                 start: 0, | ||||
|                 end: 4 | ||||
|                 end: 1 | ||||
|             }); | ||||
|  | ||||
|             expect(openmct.time.timeSystem().key).toBe(UTC_SYSTEM_AND_FORMAT_KEY); | ||||
|   | ||||
| @@ -75,7 +75,7 @@ export default { | ||||
|                 event.preventDefault(); | ||||
|                 this.preview(); | ||||
|             } else { | ||||
|                 window.location.assign(this.objectLink); | ||||
|                 this.openmct.router.navigate(this.objectLink); | ||||
|             } | ||||
|         }, | ||||
|         preview() { | ||||
|   | ||||
| @@ -180,16 +180,16 @@ export default { | ||||
|         }, | ||||
|         hasParent() { | ||||
|             return this.domainObject !== PLACEHOLDER_OBJECT | ||||
|                     && this.parentUrl !== '#/browse'; | ||||
|                     && this.parentUrl !== '/browse'; | ||||
|         }, | ||||
|         parentUrl() { | ||||
|             let objectKeyString = this.openmct.objects.makeKeyString(this.domainObject.identifier); | ||||
|             let hash = window.location.hash; | ||||
|             const objectKeyString = this.openmct.objects.makeKeyString(this.domainObject.identifier); | ||||
|             const hash = this.openmct.router.getCurrentLocation().path; | ||||
|  | ||||
|             return hash.slice(0, hash.lastIndexOf('/' + objectKeyString)); | ||||
|         }, | ||||
|         type() { | ||||
|             let objectType = this.openmct.types.get(this.domainObject.type); | ||||
|             const objectType = this.openmct.types.get(this.domainObject.type); | ||||
|             if (!objectType) { | ||||
|                 return {}; | ||||
|             } | ||||
| @@ -288,7 +288,7 @@ export default { | ||||
|                 message: 'Any unsaved changes will be lost. Are you sure you want to continue?', | ||||
|                 buttons: [ | ||||
|                     { | ||||
|                         label: 'Ok', | ||||
|                         label: 'OK', | ||||
|                         emphasis: true, | ||||
|                         callback: () => { | ||||
|                             this.openmct.editor.cancel().then(() => { | ||||
| @@ -336,7 +336,7 @@ export default { | ||||
|             }); | ||||
|         }, | ||||
|         goToParent() { | ||||
|             window.location.hash = this.parentUrl; | ||||
|             this.openmct.router.navigate(this.parentUrl); | ||||
|         }, | ||||
|         updateActionItems(actionItems) { | ||||
|             this.statusBarItems = this.actionCollection.getStatusBarActions(); | ||||
|   | ||||
| @@ -23,23 +23,7 @@ | ||||
|  | ||||
| const LocationBar = require('location-bar'); | ||||
| const EventEmitter = require('EventEmitter'); | ||||
|  | ||||
| function paramsToObject(searchParams) { | ||||
|     let params = {}; | ||||
|     for (let [key, value] of searchParams.entries()) { | ||||
|         if (params[key]) { | ||||
|             if (!Array.isArray(params[key])) { | ||||
|                 params[key] = [params[key]]; | ||||
|             } | ||||
|  | ||||
|             params[key].push(value); | ||||
|         } else { | ||||
|             params[key] = value; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     return params; | ||||
| } | ||||
| const _ = require('lodash'); | ||||
|  | ||||
| class ApplicationRouter extends EventEmitter { | ||||
|     /** | ||||
| @@ -57,11 +41,158 @@ class ApplicationRouter extends EventEmitter { | ||||
|      * route(path, handler); | ||||
|      * start(); Start routing. | ||||
|      */ | ||||
|     constructor() { | ||||
|     constructor(openmct) { | ||||
|         super(); | ||||
|  | ||||
|         this.locationBar = new LocationBar(); | ||||
|         this.openmct = openmct; | ||||
|         this.routes = []; | ||||
|         this.started = false; | ||||
|         this.locationBar = new LocationBar(); | ||||
|  | ||||
|         this.setHash = _.debounce(this.setHash.bind(this), 300); | ||||
|     } | ||||
|  | ||||
|     // Public Methods | ||||
|  | ||||
|     destroy() { | ||||
|         this.locationBar.stop(); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Delete a given query parameter from current url | ||||
|      * | ||||
|      * @param {string} paramName name of searchParam to delete from current url searchParams | ||||
|      */ | ||||
|     deleteSearchParam(paramName) { | ||||
|         let url = this.getHashRelativeURL(); | ||||
|  | ||||
|         url.searchParams.delete(paramName); | ||||
|         this.setLocationFromUrl(); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * object for accessing all current search parameters | ||||
|      * | ||||
|      * @returns {URLSearchParams} A {@link https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams/entries|URLSearchParams} | ||||
|      */ | ||||
|     getAllSearchParams() { | ||||
|         return this.getHashRelativeURL().searchParams; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Uniquely identifies a domain object. | ||||
|      * | ||||
|      * @typedef CurrentLocation | ||||
|      * @property {URL} url current url location | ||||
|      * @property {string} path current url location pathname | ||||
|      * @property {string} getQueryString a function which returns url search query | ||||
|      * @property {object} params object representing url searchParams | ||||
|      */ | ||||
|  | ||||
|     /** | ||||
|      * object for accessing current url location and search params | ||||
|      * | ||||
|      * @returns {CurrentLocation} A {@link CurrentLocation} | ||||
|      */ | ||||
|     getCurrentLocation() { | ||||
|         return this.currentLocation; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Get current location URL Object | ||||
|      * | ||||
|      * @returns {URL} current url location | ||||
|      */ | ||||
|     getHashRelativeURL() { | ||||
|         return this.getCurrentLocation().url; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Get current location URL Object searchParams | ||||
|      * | ||||
|      * @returns {object} object representing current url searchParams | ||||
|      */ | ||||
|     getParams() { | ||||
|         return this.currentLocation.params; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Get a value of given param from current url searchParams | ||||
|      * | ||||
|      * @returns {string} value of paramName from current url searchParams | ||||
|      */ | ||||
|     getSearchParam(paramName) { | ||||
|         return this.getAllSearchParams().get(paramName); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Navgate to given hash and update current location object and notify listeners about location change | ||||
|      * | ||||
|      * @param {string} paramName name of searchParam to get from current url searchParams | ||||
|      * | ||||
|      * @returns {string} value of paramName from current url searchParams | ||||
|      */ | ||||
|     navigate(hash) { | ||||
|         this.handleLocationChange(hash.substring(1)); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Add routes listeners | ||||
|      * | ||||
|      * @param {string} matcher Regex to match value in url | ||||
|      * @param {@function} callback function called when found match in url | ||||
|      */ | ||||
|     route(matcher, callback) { | ||||
|         this.routes.push({ | ||||
|             matcher, | ||||
|             callback | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Set url hash using path and queryString | ||||
|      * | ||||
|      * @param {string} path path for url | ||||
|      * @param {string} queryString queryString for url | ||||
|      */ | ||||
|     set(path, queryString) { | ||||
|         this.setHash(`${path}?${queryString}`); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Will replace all current search parameters with the ones defined in urlSearchParams | ||||
|      */ | ||||
|     setAllSearchParams() { | ||||
|         this.setLocationFromUrl(); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * To force update url based on value in currentLocation object | ||||
|      */ | ||||
|     setLocationFromUrl() { | ||||
|         this.updateTimeSettings(); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Set url hash using path | ||||
|      * | ||||
|      * @param {string} path path for url | ||||
|      */ | ||||
|     setPath(path) { | ||||
|         this.handleLocationChange(path.substring(1)); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Update param value from current url searchParams | ||||
|      * | ||||
|      * @param {string} paramName param name from current url searchParams | ||||
|      * @param {string} paramValue param value from current url searchParams | ||||
|      */ | ||||
|     setSearchParam(paramName, paramValue) { | ||||
|         let url = this.getHashRelativeURL(); | ||||
|  | ||||
|         url.searchParams.set(paramName, paramValue); | ||||
|         this.setLocationFromUrl(); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -74,105 +205,18 @@ class ApplicationRouter extends EventEmitter { | ||||
|  | ||||
|         this.started = true; | ||||
|  | ||||
|         this.locationBar.onChange(p => this.handleLocationChange(p)); | ||||
|         this.locationBar.onChange(p => this.hashChaged(p)); | ||||
|         this.locationBar.start({ | ||||
|             root: location.pathname | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     destroy() { | ||||
|         this.locationBar.stop(); | ||||
|         this.removeAllListeners(); | ||||
|     } | ||||
|  | ||||
|     handleLocationChange(pathString) { | ||||
|         if (pathString[0] !== '/') { | ||||
|             pathString = '/' + pathString; | ||||
|         } | ||||
|  | ||||
|         let url = new URL( | ||||
|             pathString, | ||||
|             `${location.protocol}//${location.host}${location.pathname}` | ||||
|         ); | ||||
|  | ||||
|         let oldLocation = this.currentLocation; | ||||
|  | ||||
|         let newLocation = { | ||||
|             url: url, | ||||
|             path: url.pathname, | ||||
|             queryString: url.search.replace(/^\?/, ''), | ||||
|             params: paramsToObject(url.searchParams) | ||||
|         }; | ||||
|  | ||||
|         this.currentLocation = newLocation; | ||||
|  | ||||
|         if (!oldLocation) { | ||||
|             this.doPathChange(newLocation.path, null, newLocation); | ||||
|             this.doParamsChange(newLocation.params, {}, newLocation); | ||||
|  | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         if (oldLocation.path !== newLocation.path) { | ||||
|             this.doPathChange( | ||||
|                 newLocation.path, | ||||
|                 oldLocation.path, | ||||
|                 this | ||||
|             ); | ||||
|         } | ||||
|  | ||||
|         if (!_.isEqual(oldLocation.params, newLocation.params)) { | ||||
|             this.doParamsChange( | ||||
|                 newLocation.params, | ||||
|                 oldLocation.params, | ||||
|                 newLocation | ||||
|             ); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     doPathChange(newPath, oldPath, newLocation) { | ||||
|         let route = this.routes.filter(r => r.matcher.test(newPath))[0]; | ||||
|         if (route) { | ||||
|             route.callback(newPath, route.matcher.exec(newPath), this.currentLocation.params); | ||||
|         } | ||||
|  | ||||
|         this.emit('change:path', newPath, oldPath); | ||||
|     } | ||||
|  | ||||
|     doParamsChange(newParams, oldParams, newLocation) { | ||||
|         let changedParams = {}; | ||||
|         Object.entries(newParams).forEach(([key, value]) => { | ||||
|             if (value !== oldParams[key]) { | ||||
|                 changedParams[key] = value; | ||||
|             } | ||||
|         }); | ||||
|         Object.keys(oldParams).forEach(key => { | ||||
|             if (!Object.prototype.hasOwnProperty.call(newParams, key)) { | ||||
|                 changedParams[key] = undefined; | ||||
|             } | ||||
|         }); | ||||
|         this.emit('change:params', newParams, oldParams, changedParams); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Update route params.  Takes an object of updates.  New parameters | ||||
|      * Set url hash using path and searchParams object | ||||
|      * | ||||
|      * @param {string} path path for url | ||||
|      * @param {string} params oject representing searchParams key/value | ||||
|      */ | ||||
|     updateParams(updateParams) { | ||||
|         let searchParams = this.currentLocation.url.searchParams; | ||||
|         Object.entries(updateParams).forEach(([key, value]) => { | ||||
|             if (typeof value === 'undefined') { | ||||
|                 searchParams.delete(key); | ||||
|             } else { | ||||
|                 searchParams.set(key, value); | ||||
|             } | ||||
|         }); | ||||
|         this.setQueryString(searchParams.toString()); | ||||
|     } | ||||
|  | ||||
|     getParams() { | ||||
|         return this.currentLocation.params; | ||||
|     } | ||||
|  | ||||
|     update(path, params) { | ||||
|         let searchParams = this.currentLocation.url.searchParams; | ||||
|         for (let [key, value] of Object.entries(params)) { | ||||
| @@ -186,24 +230,190 @@ class ApplicationRouter extends EventEmitter { | ||||
|         this.set(path, searchParams.toString()); | ||||
|     } | ||||
|  | ||||
|     set(path, queryString) { | ||||
|         location.hash = `${path}?${queryString}`; | ||||
|     } | ||||
|  | ||||
|     setQueryString(queryString) { | ||||
|         this.set(this.currentLocation.path, queryString); | ||||
|     } | ||||
|  | ||||
|     setPath(path) { | ||||
|         this.set(path, this.currentLocation.queryString); | ||||
|     } | ||||
|  | ||||
|     route(matcher, callback) { | ||||
|         this.routes.push({ | ||||
|             matcher, | ||||
|             callback | ||||
|     /** | ||||
|      * Update route params. Takes an object of updates.  New parameters | ||||
|      */ | ||||
|     updateParams(updateParams) { | ||||
|         let searchParams = this.currentLocation.url.searchParams; | ||||
|         Object.entries(updateParams).forEach(([key, value]) => { | ||||
|             if (typeof value === 'undefined') { | ||||
|                 searchParams.delete(key); | ||||
|             } else { | ||||
|                 searchParams.set(key, value); | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|         this.setQueryString(searchParams.toString()); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * To force update url based on value in currentLocation object | ||||
|      */ | ||||
|     updateTimeSettings() { | ||||
|         const hash = `${this.currentLocation.path}?${this.currentLocation.getQueryString()}`; | ||||
|  | ||||
|         this.setHash(hash); | ||||
|     } | ||||
|  | ||||
|     // Private Methods | ||||
|  | ||||
|     /** | ||||
|      * @private | ||||
|      * Create currentLocation object | ||||
|      * | ||||
|      * @param {string} pathString USVString representing relative URL. | ||||
|      * | ||||
|      * @returns {CurrentLocation} A {@link CurrentLocation} | ||||
|      */ | ||||
|     createLocation(pathString) { | ||||
|         if (pathString[0] !== '/') { | ||||
|             pathString = '/' + pathString; | ||||
|         } | ||||
|  | ||||
|         let url = new URL( | ||||
|             pathString, | ||||
|             `${location.protocol}//${location.host}${location.pathname}` | ||||
|         ); | ||||
|  | ||||
|         return { | ||||
|             url: url, | ||||
|             path: url.pathname, | ||||
|             getQueryString: () => url.search.replace(/^\?/, ''), | ||||
|             params: paramsToObject(url.searchParams) | ||||
|         }; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @private | ||||
|      * Compare new and old path and on change emit event 'change:path' | ||||
|      * | ||||
|      * @param {string} newPath new path of url | ||||
|      * @param {string} oldPath old path of url | ||||
|      */ | ||||
|     doPathChange(newPath, oldPath) { | ||||
|         if (newPath === oldPath) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         let route = this.routes.filter(r => r.matcher.test(newPath))[0]; | ||||
|         if (route) { | ||||
|             route.callback(newPath, route.matcher.exec(newPath), this.currentLocation.params); | ||||
|         } | ||||
|  | ||||
|         this.emit('change:path', newPath, oldPath); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @private | ||||
|      * Compare new and old params and on change emit event 'change:params' | ||||
|      * | ||||
|      * @param {object} newParams new params of url | ||||
|      * @param {object} oldParams old params of url | ||||
|      */ | ||||
|     doParamsChange(newParams, oldParams) { | ||||
|         if (_.isEqual(newParams, oldParams)) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         let changedParams = {}; | ||||
|         Object.entries(newParams).forEach(([key, value]) => { | ||||
|             if (value !== oldParams[key]) { | ||||
|                 changedParams[key] = value; | ||||
|             } | ||||
|         }); | ||||
|         Object.keys(oldParams).forEach(key => { | ||||
|             if (!Object.prototype.hasOwnProperty.call(newParams, key)) { | ||||
|                 changedParams[key] = undefined; | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|         this.emit('change:params', newParams, oldParams, changedParams); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @private | ||||
|      * On location change, update currentLocation object and emit appropriate events | ||||
|      * | ||||
|      * @param {string} pathString USVString representing relative URL. | ||||
|      */ | ||||
|     handleLocationChange(pathString) { | ||||
|         let oldLocation = this.currentLocation; | ||||
|         let newLocation = this.createLocation(pathString); | ||||
|  | ||||
|         this.currentLocation = newLocation; | ||||
|  | ||||
|         if (!oldLocation) { | ||||
|             this.doPathChange(newLocation.path, null); | ||||
|             this.doParamsChange(newLocation.params, {}); | ||||
|  | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         this.doPathChange( | ||||
|             newLocation.path, | ||||
|             oldLocation.path | ||||
|         ); | ||||
|  | ||||
|         this.doParamsChange( | ||||
|             newLocation.params, | ||||
|             oldLocation.params | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @private | ||||
|      * On hash changed, update currentLocation object and emit appropriate events | ||||
|      * | ||||
|      * @param {string} hash new hash for url | ||||
|      */ | ||||
|     hashChaged(hash) { | ||||
|         this.emit('change:hash', hash); | ||||
|         this.handleLocationChange(hash); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @private | ||||
|      * Set new hash for url | ||||
|      * | ||||
|      * @param {string} hash new hash for url | ||||
|      */ | ||||
|     setHash(hash) { | ||||
|         location.hash = '#' + hash.replace(/#/g, ''); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @private | ||||
|      * Set queryString part of current url | ||||
|      * | ||||
|      * @param {string} queryString queryString part of url | ||||
|      */ | ||||
|     setQueryString(queryString) { | ||||
|         this.handleLocationChange(`${this.currentLocation.path}?${queryString}`); | ||||
|     } | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Convert searchParams into Object | ||||
|  * | ||||
|  * @param {URLSearchParams} searchParams queryString part of url | ||||
|  * | ||||
|  * @returns {Object} | ||||
|  */ | ||||
| function paramsToObject(searchParams) { | ||||
|     let params = {}; | ||||
|     for (let [key, value] of searchParams.entries()) { | ||||
|         if (params[key]) { | ||||
|             if (!Array.isArray(params[key])) { | ||||
|                 params[key] = [params[key]]; | ||||
|             } | ||||
|  | ||||
|             params[key].push(value); | ||||
|         } else { | ||||
|             params[key] = value; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     return params; | ||||
| } | ||||
|  | ||||
| module.exports = ApplicationRouter; | ||||
|   | ||||
							
								
								
									
										139
									
								
								src/ui/router/ApplicationRouterSpec.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										139
									
								
								src/ui/router/ApplicationRouterSpec.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,139 @@ | ||||
| import { createOpenMct, resetApplicationState } from 'utils/testing'; | ||||
|  | ||||
| let openmct; | ||||
| let element; | ||||
| let child; | ||||
| let appHolder; | ||||
| let resolveFunction; | ||||
|  | ||||
| let initialHash = ''; | ||||
|  | ||||
| describe('Application router utility functions', () => { | ||||
|     beforeAll(done => { | ||||
|         appHolder = document.createElement('div'); | ||||
|         appHolder.style.width = '640px'; | ||||
|         appHolder.style.height = '480px'; | ||||
|  | ||||
|         openmct = createOpenMct(); | ||||
|         openmct.install(openmct.plugins.MyItems()); | ||||
|         openmct.install(openmct.plugins.LocalTimeSystem()); | ||||
|         openmct.install(openmct.plugins.UTCTimeSystem()); | ||||
|  | ||||
|         element = document.createElement('div'); | ||||
|         child = document.createElement('div'); | ||||
|         element.appendChild(child); | ||||
|  | ||||
|         openmct.on('start', done); | ||||
|         openmct.start(appHolder); | ||||
|  | ||||
|         document.body.append(appHolder); | ||||
|     }); | ||||
|  | ||||
|     afterAll(() => { | ||||
|         openmct.router.setHash(initialHash); | ||||
|         appHolder.remove(); | ||||
|  | ||||
|         return resetApplicationState(openmct); | ||||
|     }); | ||||
|  | ||||
|     it('has initial hash when loaded', (done) => { | ||||
|         let success; | ||||
|         resolveFunction = () => { | ||||
|             openmct.router.setLocationFromUrl(); | ||||
|             success = window.location.hash !== null; | ||||
|             if (success) { | ||||
|                 initialHash = window.location.hash; | ||||
|                 expect(success).toBe(true); | ||||
|  | ||||
|                 openmct.router.removeListener('change:hash', resolveFunction); | ||||
|                 done(); | ||||
|             } | ||||
|         }; | ||||
|  | ||||
|         openmct.router.on('change:hash', resolveFunction); | ||||
|     }); | ||||
|  | ||||
|     it('The setSearchParam function sets an individual search parameter in the window location hash', (done) => { | ||||
|         let success; | ||||
|         openmct.router.setSearchParam('testParam', 'testValue'); | ||||
|         resolveFunction = () => { | ||||
|             success = window.location.hash.includes('testParam=testValue'); | ||||
|             if (success) { | ||||
|                 expect(success).toBe(true); | ||||
|  | ||||
|                 openmct.router.removeListener('change:hash', resolveFunction); | ||||
|                 done(); | ||||
|             } | ||||
|         }; | ||||
|  | ||||
|         openmct.router.on('change:hash', resolveFunction); | ||||
|     }); | ||||
|  | ||||
|     it('The getSearchParam function returns the value of an individual search paramater in the window location hash', () => { | ||||
|         expect(openmct.router.getSearchParam('testParam')).toBe('testValue'); | ||||
|     }); | ||||
|  | ||||
|     it('The deleteSearchParam function deletes an individual search paramater in the window location hash', (done) => { | ||||
|         let success; | ||||
|         openmct.router.deleteSearchParam('testParam'); | ||||
|         resolveFunction = () => { | ||||
|             success = window.location.hash.includes('testParam=testValue') === false; | ||||
|             if (success) { | ||||
|                 expect(success).toBe(true); | ||||
|  | ||||
|                 openmct.router.removeListener('change:hash', resolveFunction); | ||||
|                 done(); | ||||
|             } | ||||
|         }; | ||||
|  | ||||
|         openmct.router.on('change:hash', resolveFunction); | ||||
|     }); | ||||
|  | ||||
|     it('The setSearchParam function sets an individual search parameters in the window location hash', (done) => { | ||||
|         let success; | ||||
|         openmct.router.setSearchParam('testParam1', 'testValue1'); | ||||
|         openmct.router.setSearchParam('testParam2', 'testValue2'); | ||||
|  | ||||
|         resolveFunction = () => { | ||||
|             const hasTestParam1 = window.location.hash.includes('testParam1=testValue1'); | ||||
|             const hasTestParam2 = window.location.hash.includes('testParam2=testValue2'); | ||||
|             success = hasTestParam1 && hasTestParam2; | ||||
|             if (success) { | ||||
|                 expect(success).toBe(true); | ||||
|  | ||||
|                 openmct.router.removeListener('change:hash', resolveFunction); | ||||
|                 done(); | ||||
|             } | ||||
|         }; | ||||
|  | ||||
|         openmct.router.on('change:hash', resolveFunction); | ||||
|     }); | ||||
|  | ||||
|     it('The setAllSearchParams function replaces all search paramaters in the window location hash', (done) => { | ||||
|         let success; | ||||
|  | ||||
|         openmct.router.setSearchParam('testParam2', 'updatedtestValue2'); | ||||
|         openmct.router.setSearchParam('newTestParam3', 'newTestValue3'); | ||||
|  | ||||
|         resolveFunction = () => { | ||||
|             const hasupdatedValueForTestParam2 = window.location.hash.includes('testParam2=updatedtestValue2'); | ||||
|             const hasNewTestParam3 = window.location.hash.includes('newTestParam3=newTestValue3'); | ||||
|             success = hasupdatedValueForTestParam2 && hasNewTestParam3; | ||||
|             if (success) { | ||||
|                 expect(success).toBe(true); | ||||
|  | ||||
|                 openmct.router.removeListener('change:hash', resolveFunction); | ||||
|                 done(); | ||||
|             } | ||||
|         }; | ||||
|  | ||||
|         openmct.router.on('change:hash', resolveFunction); | ||||
|     }); | ||||
|  | ||||
|     it('The getAllSearchParams function returns the values of all search paramaters in the window location hash', () => { | ||||
|         let searchParams = openmct.router.getAllSearchParams(); | ||||
|         expect(searchParams.get('testParam1')).toBe('testValue1'); | ||||
|         expect(searchParams.get('testParam2')).toBe('updatedtestValue2'); | ||||
|         expect(searchParams.get('newTestParam3')).toBe('newTestValue3'); | ||||
|     }); | ||||
| }); | ||||
| @@ -13,13 +13,12 @@ define([ | ||||
|         let mutable; | ||||
|  | ||||
|         openmct.router.route(/^\/browse\/?$/, navigateToFirstChildOfRoot); | ||||
|  | ||||
|         openmct.router.route(/^\/browse\/(.*)$/, (path, results, params) => { | ||||
|             isRoutingInProgress = true; | ||||
|             let navigatePath = results[1]; | ||||
|             clearMutationListeners(); | ||||
|  | ||||
|             navigateToPath(navigatePath, params.view); | ||||
|             onParamsChanged(null, null, params); | ||||
|         }); | ||||
|  | ||||
|         openmct.router.on('change:params', onParamsChanged); | ||||
| @@ -133,18 +132,21 @@ define([ | ||||
|         } | ||||
|  | ||||
|         function navigateToFirstChildOfRoot() { | ||||
|             openmct.objects.get('ROOT').then(rootObject => { | ||||
|                 openmct.composition.get(rootObject).load() | ||||
|                     .then(children => { | ||||
|                         let lastChild = children[children.length - 1]; | ||||
|                         if (!lastChild) { | ||||
|                             console.error('Unable to navigate to anything. No root objects found.'); | ||||
|                         } else { | ||||
|                             let lastChildId = openmct.objects.makeKeyString(lastChild.identifier); | ||||
|                             openmct.router.setPath(`#/browse/${lastChildId}`); | ||||
|                         } | ||||
|                     }); | ||||
|             }); | ||||
|             openmct.objects.get('ROOT') | ||||
|                 .then(rootObject => { | ||||
|                     openmct.composition.get(rootObject).load() | ||||
|                         .then(children => { | ||||
|                             let lastChild = children[children.length - 1]; | ||||
|                             if (!lastChild) { | ||||
|                                 console.error('Unable to navigate to anything. No root objects found.'); | ||||
|                             } else { | ||||
|                                 let lastChildId = openmct.objects.makeKeyString(lastChild.identifier); | ||||
|                                 openmct.router.setPath(`#/browse/${lastChildId}`); | ||||
|                             } | ||||
|                         }) | ||||
|                         .catch(e => console.error(e)); | ||||
|                 }) | ||||
|                 .catch(e => console.error(e)); | ||||
|         } | ||||
|  | ||||
|         function clearMutationListeners() { | ||||
|   | ||||
| @@ -1,108 +0,0 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2014-2021, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
|  * Administration. All rights reserved. | ||||
|  * | ||||
|  * Open MCT is licensed under the Apache License, Version 2.0 (the | ||||
|  * "License"); you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0. | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||||
|  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||||
|  * License for the specific language governing permissions and limitations | ||||
|  * under the License. | ||||
|  * | ||||
|  * Open MCT includes source code licensed under additional open source | ||||
|  * licenses. See the Open Source Licenses file (LICENSES.md) included with | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| import objectUtils from '../api/objects/object-utils.js'; | ||||
|  | ||||
| /** | ||||
|  * Utility functions for getting and setting Open MCT search parameters and navigated object path. | ||||
|  * Open MCT encodes application state into the "hash" of the url, making it awkward to use standard browser API such | ||||
|  * as URL for modifying state in the URL. This wraps native API with some utility functions that operate only on the | ||||
|  * hash section of the URL. | ||||
|  */ | ||||
|  | ||||
| export function setSearchParam(paramName, paramValue) { | ||||
|     let url = getHashRelativeURL(); | ||||
|  | ||||
|     url.searchParams.set(paramName, paramValue); | ||||
|     setLocationFromUrl(url); | ||||
| } | ||||
|  | ||||
| export function deleteSearchParam(paramName) { | ||||
|     let url = getHashRelativeURL(); | ||||
|  | ||||
|     url.searchParams.delete(paramName); | ||||
|     setLocationFromUrl(url); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Will replace all current search parameters with the ones defined in urlSearchParams | ||||
|  * @param {URLSearchParams} paramMap | ||||
|  */ | ||||
| export function setAllSearchParams(newSearchParams) { | ||||
|     let url = getHashRelativeURL(); | ||||
|  | ||||
|     Array.from(url.searchParams.keys()).forEach((key) => url.searchParams.delete(key)); | ||||
|  | ||||
|     Array.from(newSearchParams.keys()).forEach(key => { | ||||
|         url.searchParams.set(key, newSearchParams.get(key)); | ||||
|     }); | ||||
|  | ||||
|     setLocationFromUrl(url); | ||||
| } | ||||
|  | ||||
| export function getSearchParam(paramName) { | ||||
|     return getAllSearchParams().get(paramName); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * @returns {URLSearchParams} A {@link https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams/entries|URLSearchParams} | ||||
|  * object for accessing all current search parameters | ||||
|  */ | ||||
| export function getAllSearchParams() { | ||||
|     return getHashRelativeURL().searchParams; | ||||
| } | ||||
|  | ||||
| export function getObjectPath() { | ||||
|     return getHashRelativeURL().pathname; | ||||
| } | ||||
|  | ||||
| export function setObjectPath(objectPath) { | ||||
|     let objectPathString; | ||||
|     let url = getHashRelativeURL(); | ||||
|  | ||||
|     if (objectPath instanceof Array) { | ||||
|         if (objectPath.length > 0 && isDomainObject(objectPath[0])) { | ||||
|             throw 'setObjectPath must be called with either a string, or an array of Domain Objects'; | ||||
|         } | ||||
|  | ||||
|         objectPathString = objectPath.reduce((pathString, object) => { | ||||
|             return `${pathString}/${objectUtils.makeKeyString(object.identifier)}`; | ||||
|         }, ''); | ||||
|     } else { | ||||
|         objectPathString = objectPath; | ||||
|     } | ||||
|  | ||||
|     url.pathname = objectPathString; | ||||
|     setLocationFromUrl(url); | ||||
| } | ||||
|  | ||||
| function isDomainObject(potentialObject) { | ||||
|     return potentialObject.identifier === undefined; | ||||
| } | ||||
|  | ||||
| function setLocationFromUrl(url) { | ||||
|     window.location.hash = `${url.pathname}${url.search}`; | ||||
| } | ||||
|  | ||||
| function getHashRelativeURL() { | ||||
|     return new URL(window.location.hash.substring(1), window.location.origin); | ||||
| } | ||||
| @@ -1,113 +0,0 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2014-2021, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
|  * Administration. All rights reserved. | ||||
|  * | ||||
|  * Open MCT is licensed under the Apache License, Version 2.0 (the | ||||
|  * "License"); you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0. | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||||
|  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||||
|  * License for the specific language governing permissions and limitations | ||||
|  * under the License. | ||||
|  * | ||||
|  * Open MCT includes source code licensed under additional open source | ||||
|  * licenses. See the Open Source Licenses file (LICENSES.md) included with | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
| import { | ||||
|     setSearchParam, | ||||
|     deleteSearchParam, | ||||
|     getAllSearchParams, | ||||
|     getSearchParam, | ||||
|     setAllSearchParams, | ||||
|     getObjectPath, | ||||
|     setObjectPath | ||||
| } from './openmctLocation'; | ||||
|  | ||||
| import {resetApplicationState} from 'utils/testing'; | ||||
|  | ||||
| describe('the openmct location utility functions', () => { | ||||
|     afterEach(() => resetApplicationState()); | ||||
|  | ||||
|     it('The setSearchParam function sets an individual search parameters in the window location hash', () => { | ||||
|         setSearchParam('testParam', 'testValue'); | ||||
|         expect(window.location.hash.includes('testParam=testValue')).toBe(true); | ||||
|     }); | ||||
|  | ||||
|     it('The deleteSearchParam function deletes an individual search paramater in the window location hash', () => { | ||||
|         window.location.hash = '#/?testParam=testValue'; | ||||
|         deleteSearchParam('testParam'); | ||||
|         expect(window.location.hash.includes('testParam=testValue')).toBe(false); | ||||
|     }); | ||||
|  | ||||
|     it('The getSearchParam function returns the value of an individual search paramater in the window location hash', () => { | ||||
|         window.location.hash = '#/?testParam=testValue'; | ||||
|         expect(getSearchParam('testParam')).toBe('testValue'); | ||||
|     }); | ||||
|  | ||||
|     it('The getAllSearchParams function returns the values of all search paramaters in the window location hash', () => { | ||||
|         window.location.hash = '#/?testParam1=testValue1&testParam2=testValue2&testParam3=testValue3'; | ||||
|         let searchParams = getAllSearchParams(); | ||||
|         expect(searchParams.get('testParam1')).toBe('testValue1'); | ||||
|         expect(searchParams.get('testParam2')).toBe('testValue2'); | ||||
|         expect(searchParams.get('testParam3')).toBe('testValue3'); | ||||
|     }); | ||||
|  | ||||
|     it('The setAllSearchParams function replaces all search paramaters in the window location hash', () => { | ||||
|         window.location.hash = '#/?testParam1=testValue1&testParam2=testValue2&testParam3=testValue3'; | ||||
|         let searchParams = getAllSearchParams(); | ||||
|         searchParams.delete('testParam3'); | ||||
|         searchParams.set('testParam1', 'updatedTestValue1'); | ||||
|         searchParams.set('newTestParam4', 'newTestValue4'); | ||||
|         setAllSearchParams(searchParams); | ||||
|         expect(window.location.hash).toBe('#/?testParam1=updatedTestValue1&testParam2=testValue2&newTestParam4=newTestValue4'); | ||||
|     }); | ||||
|  | ||||
|     it('The getObjectPath function returns the current object path', () => { | ||||
|         window.location.hash = '#/some/object/path?someParameter=someValue'; | ||||
|         expect(getObjectPath()).toBe('/some/object/path'); | ||||
|     }); | ||||
|  | ||||
|     it('The setObjectPath function allows the object path to be set to a given string', () => { | ||||
|         window.location.hash = '#/some/object/path?someParameter=someValue'; | ||||
|         setObjectPath('/some/other/object/path'); | ||||
|         expect(window.location.hash).toBe('#/some/other/object/path?someParameter=someValue'); | ||||
|     }); | ||||
|  | ||||
|     it('The setObjectPath function allows the object path to be set from an array of domain objects', () => { | ||||
|         const OBJECT_PATH = [ | ||||
|             { | ||||
|                 identifier: { | ||||
|                     namespace: 'namespace', | ||||
|                     key: 'objectKey1' | ||||
|                 } | ||||
|             }, | ||||
|             { | ||||
|                 identifier: { | ||||
|                     namespace: 'namespace', | ||||
|                     key: 'objectKey2' | ||||
|                 } | ||||
|             }, | ||||
|             { | ||||
|                 identifier: { | ||||
|                     namespace: 'namespace', | ||||
|                     key: 'objectKey3' | ||||
|                 } | ||||
|             } | ||||
|         ]; | ||||
|         window.location.hash = '#/some/object/path?someParameter=someValue'; | ||||
|         setObjectPath(OBJECT_PATH); | ||||
|         expect(window.location.hash).toBe('#/namespace:objectKey1/namespace:objectKey2/namespace:objectKey3?someParameter=someValue'); | ||||
|     }); | ||||
|  | ||||
|     it('The setObjectPath function throws an error if called with anything other than a string or an array of domain objects', () => { | ||||
|         expect(() => setObjectPath(["array", "of", "strings"])).toThrow(); | ||||
|         expect(() => setObjectPath([{}, {someKey: 'someValue'}])).toThrow(); | ||||
|     }); | ||||
| }); | ||||
|  | ||||
| @@ -64,21 +64,27 @@ const webpackConfig = { | ||||
|             filename: '[name].css', | ||||
|             chunkFilename: '[name].css' | ||||
|         }), | ||||
|         new CopyWebpackPlugin([ | ||||
|             { | ||||
|                 from: 'src/images/favicons', | ||||
|                 to: 'favicons' | ||||
|             }, | ||||
|             { | ||||
|                 from: './index.html', | ||||
|                 transform: function (content) { | ||||
|                     return content.toString().replace(/dist\//g, ''); | ||||
|         new CopyWebpackPlugin({ | ||||
|             patterns: [ | ||||
|                 { | ||||
|                     from: 'src/images/favicons', | ||||
|                     to: 'favicons' | ||||
|                 }, | ||||
|                 { | ||||
|                     from: './index.html', | ||||
|                     transform: function (content) { | ||||
|                         return content.toString().replace(/dist\//g, ''); | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         ]) | ||||
|             ] | ||||
|         }) | ||||
|     ], | ||||
|     module: { | ||||
|         rules: [ | ||||
|             { | ||||
|                 test: /\.vue$/, | ||||
|                 use: 'vue-loader' | ||||
|             }, | ||||
|             { | ||||
|                 test: /\.(sc|sa|c)ss$/, | ||||
|                 use: [ | ||||
| @@ -118,17 +124,14 @@ const webpackConfig = { | ||||
|                     } | ||||
|                 } | ||||
|             }, | ||||
|             { | ||||
|                 test: /\.vue$/, | ||||
|                 use: 'vue-loader' | ||||
|             } | ||||
|              | ||||
|         ] | ||||
|     }, | ||||
|     ignoreWarnings: [/asset size limit/g], | ||||
|     stats: { | ||||
|         modules: false, | ||||
|         timings: true, | ||||
|         colors: true, | ||||
|         warningsFilter: /asset size limit/g | ||||
|         colors: true | ||||
|     } | ||||
| }; | ||||
|  | ||||
|   | ||||
		Reference in New Issue
	
	Block a user