Compare commits
	
		
			159 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | 5b0c0e1278 | ||
|   | e5cf419697 | ||
|   | 019e04f9ab | ||
|   | 9b63a63efc | ||
|   | 8e68914b56 | ||
|   | f0a5a836d6 | ||
|   | 2b0f6dd577 | ||
|   | fcee30c27e | ||
|   | 560825cc86 | ||
|   | 2df61998ab | ||
|   | 91b42fc0cc | ||
|   | 5eac0bc5d9 | ||
|   | d397f0f283 | ||
|   | 9c4c020f54 | ||
|   | 08c4302011 | ||
|   | 5f96861c44 | ||
|   | 995d71f901 | ||
|   | 82e4a53472 | ||
|   | e7a41061c6 | ||
|   | 4848a61e21 | ||
|   | 6302e45c10 | ||
|   | 451157b653 | ||
|   | ae89a169d0 | ||
|   | 941d0d0057 | ||
|   | 0296cfe3c1 | ||
|   | f55168d1ac | ||
|   | 3c4fb8c43d | ||
|   | ed7e16d341 | ||
|   | 9dc958b952 | ||
|   | ed3ee1099d | ||
|   | 7fb506d4df | ||
|   | 67707678a8 | ||
|   | 930b13f9a0 | ||
|   | b59fc43038 | ||
|   | 9b32461240 | ||
|   | d1f617a54e | ||
|   | c4f99a6cab | ||
|   | 83e77303aa | ||
|   | 29a1c7dc5d | ||
|   | 65a1d7495d | ||
|   | a98ab958c9 | ||
|   | 6e1cadf338 | ||
|   | 83a9b984e5 | ||
|   | 978df93ffd | ||
|   | 33b1c1689c | ||
|   | 0cfc070f3c | ||
|   | 21fd16ddf1 | ||
|   | ce94db0a4b | ||
|   | 0d894a9f39 | ||
|   | 5ad150a17e | ||
|   | b6acdb12ec | ||
|   | 604b29096d | ||
|   | 6200ceddf8 | ||
|   | de88bf94d4 | ||
|   | 19f07aa398 | ||
|   | 46f9b31cff | ||
|   | e32feb29e2 | ||
|   | 89a93e2966 | ||
|   | 038322e9aa | ||
|   | 6cef663db5 | ||
|   | b2f5861458 | ||
|   | f8809ce67f | ||
|   | 3c97eb6014 | ||
|   | 3498b0a50a | ||
|   | 14c5a817a7 | ||
|   | 258c5d95e6 | ||
|   | 30a8ba5c11 | ||
|   | 33c372765f | ||
|   | ed7e0d8b0a | ||
|   | f3828ba516 | ||
|   | 2a48c25453 | ||
|   | 096da0cb94 | ||
|   | ca8a07e1ae | ||
|   | c02f965460 | ||
|   | 5f440bb6de | ||
|   | fc729279ec | ||
|   | dd0f9ab74e | ||
|   | d2a4a85e04 | ||
|   | 37890280ae | ||
|   | a6ceae4045 | ||
|   | 3447e735dc | ||
|   | 989c937ce1 | ||
|   | e0608ddee0 | ||
|   | b35224061f | ||
|   | b25576aed8 | ||
|   | d5f054e328 | ||
|   | eb4959cf49 | ||
|   | cce415fc51 | ||
|   | 00f96c314a | ||
|   | fefd27162c | ||
|   | b388c76e45 | ||
|   | aaeaf3a096 | ||
|   | 57efe4e0d1 | ||
|   | dd4dbc9326 | ||
|   | 6aa77ff468 | ||
|   | f8099550bd | ||
|   | 96249e6bcc | ||
|   | 6e391098a3 | ||
|   | baec0f9719 | ||
|   | 6aab9f4e34 | ||
|   | 1bf73935e4 | ||
|   | 49e51d0a62 | ||
|   | 3e7bc2f37f | ||
|   | 0f56fd2561 | ||
|   | 9f0114eb39 | ||
|   | 4b82893c36 | ||
|   | 734e979c94 | ||
|   | 983973843e | ||
|   | 3b427c31a2 | ||
|   | cee0ecf0ef | ||
|   | 8e3c5db3bf | ||
|   | 11d8daf3ed | ||
|   | 9953e16415 | ||
|   | fe600de0f7 | ||
|   | 386f1f20ff | ||
|   | 5e07951892 | ||
|   | 2514e44083 | ||
|   | 20d9c7158e | ||
|   | d8e319ebf8 | ||
|   | a39cbbd917 | ||
|   | 8df27a1c05 | ||
|   | 26cf9c14f4 | ||
|   | 03edd26e17 | ||
|   | 571f6d183a | ||
|   | 1292e39c46 | ||
|   | 6fe3f82fb1 | ||
|   | da8fb99e82 | ||
|   | c8d77bc2db | ||
|   | b5e52fce75 | ||
|   | 3afcb52934 | ||
|   | 677b0cffec | ||
|   | 248bc68f0d | ||
|   | 02050fa3ef | ||
|   | 57d23c3696 | ||
|   | 8babfc5ca9 | ||
|   | 271b5d1a73 | ||
|   | 7d4e7a0925 | ||
|   | 92f5d5f190 | ||
|   | 1731b985fc | ||
|   | bd4590ad9d | ||
|   | 55fc60ec82 | ||
|   | ab075e9ad8 | ||
|   | 3ac1710d83 | ||
|   | 730878938e | ||
|   | 3fd4304de1 | ||
|   | db7224486c | ||
|   | 424953c894 | ||
|   | 6d0f3c7faa | ||
|   | 434a52ded3 | ||
|   | ffff13205a | ||
|   | 16efd85dfc | ||
|   | 1ef09ffbdd | ||
|   | 976ecce075 | ||
|   | 87a51a9eb3 | ||
|   | c84de00e80 | ||
|   | 91997ced01 | ||
|   | 7a4be9e67e | ||
|   | eb942b0bf7 | ||
|   | 1cf23c7ad6 | 
| @@ -291,7 +291,7 @@ checklist.) | ||||
| 1. Changes address original issue? | ||||
| 2. Unit tests included and/or updated with changes? | ||||
| 3. Command line build passes? | ||||
| 4. Expect to pass code review? | ||||
| 4. Changes have been smoke-tested? | ||||
|  | ||||
| ### Reviewer Checklist | ||||
|  | ||||
|   | ||||
| @@ -106,7 +106,7 @@ GLOBAL.window = GLOBAL.window ||  GLOBAL; // nomnoml expects window to be define | ||||
|     } | ||||
|  | ||||
|     // Convert from Github-flavored Markdown to HTML | ||||
|     function gfmifier() { | ||||
|     function gfmifier(renderTOC) { | ||||
|         var transform = new stream.Transform({ objectMode: true }), | ||||
|             markdown = ""; | ||||
|         transform._transform = function (chunk, encoding, done) { | ||||
| @@ -114,9 +114,11 @@ GLOBAL.window = GLOBAL.window ||  GLOBAL; // nomnoml expects window to be define | ||||
|             done(); | ||||
|         }; | ||||
|         transform._flush = function (done) { | ||||
|             // Prepend table of contents | ||||
|             markdown = | ||||
|                 [ TOC_HEAD, toc(markdown).content, "", markdown ].join("\n"); | ||||
|             if (renderTOC){ | ||||
|                 // Prepend table of contents | ||||
|                 markdown = | ||||
|                     [ TOC_HEAD, toc(markdown).content, "", markdown ].join("\n"); | ||||
|             } | ||||
|             this.push(header); | ||||
|             this.push(marked(markdown)); | ||||
|             this.push(footer); | ||||
| @@ -168,13 +170,16 @@ GLOBAL.window = GLOBAL.window ||  GLOBAL; // nomnoml expects window to be define | ||||
|             var destination = file.replace(options['in'], options.out) | ||||
|                 .replace(/md$/, "html"), | ||||
|                 destPath = path.dirname(destination), | ||||
|                 prefix = path.basename(destination).replace(/\.html$/, ""); | ||||
|                 prefix = path.basename(destination).replace(/\.html$/, ""), | ||||
|                 //Determine whether TOC should be rendered for this file based | ||||
|                 //on regex provided as command line option | ||||
|                 renderTOC = file.match(options['suppress-toc'] || "") === null; | ||||
|  | ||||
|             mkdirp(destPath, function (err) { | ||||
|                 fs.createReadStream(file, { encoding: 'utf8' }) | ||||
|                     .pipe(split()) | ||||
|                     .pipe(nomnomlifier(destPath, prefix)) | ||||
|                     .pipe(gfmifier()) | ||||
|                     .pipe(gfmifier(renderTOC)) | ||||
|                     .pipe(fs.createWriteStream(destination, { | ||||
|                         encoding: 'utf8' | ||||
|                     })); | ||||
|   | ||||
| @@ -941,6 +941,12 @@ look at field  (see below) to determine which field in the model should be | ||||
| modified.  | ||||
| * `ngRequired`: True if input is required. | ||||
| * `ngPattern`: The pattern to match against (for text entry) | ||||
| * `ngBlur`: A function that may be invoked to evaluate the expression | ||||
|   associated with the `ng-blur` attribute associated with the control. | ||||
|   * This should be called when the control has lost focus; for controls | ||||
|     which simply wrap or augment `input` elements, this should be fired | ||||
|     on `blur` events associated with those elements, while more complex | ||||
|     custom controls may fire this at the end of more specific interactions. | ||||
| * `options`: The options for this control, as passed from the `options` property  | ||||
| of an individual row definition.  | ||||
| * `field`: Name of the field in `ngModel` which will hold the value for this  | ||||
|   | ||||
| @@ -1,38 +0,0 @@ | ||||
| <!-- | ||||
|  Open MCT Web, Copyright (c) 2014-2015, United States Government | ||||
|  as represented by the Administrator of the National Aeronautics and Space | ||||
|  Administration. All rights reserved. | ||||
|  | ||||
|  Open MCT Web 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 Web 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. | ||||
| --> | ||||
| <!DOCTYPE html> | ||||
| <html> | ||||
| <head lang="en"> | ||||
|     <meta charset="UTF-8"> | ||||
|     <title>Open MCT Web Documentation</title> | ||||
| </head> | ||||
| <body class="user-environ" ng-view> | ||||
|     Sections: | ||||
|     <ul> | ||||
|         <li><a href="api/">API</a></li> | ||||
|         <li><a href="architecture/">Architecture Overview</a></li> | ||||
|         <li><a href="guide/">Developer Guide</a></li> | ||||
|         <li><a href="tutorials/">Tutorials</a></li> | ||||
|         <li><a href="process/">Development Process</a></li> | ||||
|     </ul> | ||||
| </body> | ||||
| </html> | ||||
							
								
								
									
										35
									
								
								docs/src/index.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								docs/src/index.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,35 @@ | ||||
| # Open MCT Web Documentation | ||||
|  | ||||
| ## Overview | ||||
|  | ||||
|  Documentation is provided to support the use and development of | ||||
|  Open MCT Web. It's recommended that before doing | ||||
|  any development with Open MCT Web you take some time to familiarize yourself  | ||||
|  with the documentation below. | ||||
|  | ||||
|  Open MCT Web provides functionality out of the box, but it's also a platform for | ||||
|  building rich mission operations applications based on modern web technology.  | ||||
|  The platform is configured declaratively, and defines conventions for | ||||
|  building on the provided capabilities by creating modular 'bundles' that  | ||||
|  extend the platform at a variety of extension points. The details of how to | ||||
|  extend the platform are provided in the following documentation. | ||||
|  | ||||
| ## Sections | ||||
|  | ||||
|  * The [Architecture Overview](architecture/) describes the concepts used  | ||||
|  throughout Open MCT Web, and gives a high level overview of the platform's design. | ||||
|   | ||||
|  * The [Developer's Guide](guide/) goes into more detail about how to use the | ||||
|  platform and the functionality that it provides. | ||||
|   | ||||
|  * The [Tutorials](tutorials/) give examples of extending the platform to add  | ||||
|  functionality,  | ||||
|  and integrate with data sources. | ||||
|   | ||||
|  * The [API](api/) document is generated from inline documentation  | ||||
|  using [JSDoc](http://usejsdoc.org/), and describes the JavaScript objects and | ||||
|  functions that make up the software platform. | ||||
|  | ||||
|  * Finally, the [Development Process](process/) document describes the  | ||||
|  Open MCT Web software development cycle. | ||||
|   | ||||
							
								
								
									
										161
									
								
								docs/src/process/cycle.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										161
									
								
								docs/src/process/cycle.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,161 @@ | ||||
| # Development Cycle | ||||
|  | ||||
| Development of Open MCT Web occurs on an iterative cycle of | ||||
| sprints and releases. | ||||
|  | ||||
| * A _sprint_ is three weeks in duration, and represents a | ||||
|   set of improvements that can be completed and tested by the | ||||
|   development team. Software at the end of the sprint is | ||||
|   "semi-stable"; it will have undergone reduced testing and may carry | ||||
|   defects or usability issues of lower severity, particularly if | ||||
|   there are workarounds. | ||||
| * A _release_ occurs every four sprints. Releases are stable, and | ||||
|   will have undergone full acceptance testing to ensure that the | ||||
|   software behaves correctly and usably. | ||||
|  | ||||
| ## Roles | ||||
|  | ||||
| The sprint process assumes the presence of a __project manager.__ | ||||
| The project manager is responsible for | ||||
| making tactical decisions about what development work will be | ||||
| performed, and for coordinating with stakeholders to arrive at | ||||
| higher-level strategic decisions about desired functionality | ||||
| and characteristics of the software, major external milestones, | ||||
| and so forth. | ||||
|  | ||||
| In the absence of a dedicated project manager, this role may be rotated | ||||
| among members of the development team on a per-sprint basis. | ||||
|  | ||||
| Responsibilities of the project manager including: | ||||
|  | ||||
| * Maintaining (with agreement of stakeholders) a "road map" of work | ||||
|   planned for future releases/sprints; this should be higher-level, | ||||
|   usually expressed as "themes", | ||||
|   with just enough specificity to gauge feasibility of plans, | ||||
|   relate work back to milestones, and identify longer-term | ||||
|   dependencies. | ||||
| * Determining (with assistance from the rest of the team) which | ||||
|   issues to work on in a given sprint and how they shall be | ||||
|   assigned. | ||||
| * Pre-planning subsequent sprints to ensure that all members of the | ||||
|   team always have a clear direction. | ||||
| * Scheduling and/or ensuring adherence to | ||||
|   [process points](#process-points). | ||||
| * Responding to changes within the sprint (shifting priorities, | ||||
|   new issues) and re-allocating work for the sprint as needed. | ||||
|  | ||||
| ## Sprint Calendar | ||||
|  | ||||
| Certain [process points](#process-points) are regularly scheduled in | ||||
| the sprint cycle. | ||||
|  | ||||
| ### Sprints by Release | ||||
|  | ||||
| Allocation of work among sprints should be planned relative to release | ||||
| goals and milestones. As a general guideline, higher-risk work (large | ||||
| new features which may carry new defects, major refactoring, design | ||||
| changes with uncertain effects on usability) should be allocated to | ||||
| earlier sprints, allowing for time in later sprints to ensure stability. | ||||
|  | ||||
| | Sprint | Focus                                                   | | ||||
| |:------:|:--------------------------------------------------------| | ||||
| | __1__  | Prototyping, design, experimentation.                   | | ||||
| | __2__  | New features, refinements, enhancements.                | | ||||
| | __3__  | Feature completion, low-risk enhancements, bug fixing.  | | ||||
| | __4__  | Stability & quality assurance.                          | | ||||
|  | ||||
| ### Sprints 1-3 | ||||
|  | ||||
| The first three sprints of a release are primarily centered around | ||||
| development work, with regular acceptance testing in the third | ||||
| week. During this third week, the top priority should be passing | ||||
| acceptance testing (e.g. by resolving any blockers found); any | ||||
| resources not needed for this effort should be used to begin work | ||||
| for the subsequent sprint. | ||||
|  | ||||
| | Week  | Mon                       | Tue    | Wed | Thu                          | Fri         | | ||||
| |:-----:|:-------------------------:|:------:|:---:|:----------------------------:|:-----------:| | ||||
| | __1__ | Sprint plan               | Tag-up |     |                              |             | | ||||
| | __2__ |                           | Tag-up |     |                              | Code freeze | | ||||
| | __3__ | Per-sprint testing        | Triage |     | _Per-sprint testing*_        | Ship        | | ||||
|  | ||||
| * If necessary. | ||||
|  | ||||
| ### Sprint 4 | ||||
|  | ||||
| The software must be stable at the end of the fourth sprint; because of | ||||
| this, the fourth sprint is scheduled differently, with a heightened | ||||
| emphasis on testing. | ||||
|  | ||||
| | Week   | Mon                       | Tue    | Wed | Thu                          | Fri         | | ||||
| |-------:|:-------------------------:|:------:|:---:|:----------------------------:|:-----------:| | ||||
| | __1__  | Sprint plan               | Tag-up |     |                              | Code freeze | | ||||
| | __2__  | Per-release testing       | Triage |     |                              |             | | ||||
| | __3__  | _Per-release testing*_    | Triage |     | _Per-release testing*_       | Ship        | | ||||
|  | ||||
| * If necessary. | ||||
|  | ||||
| ## Process Points | ||||
|  | ||||
| * __Sprint plan.__ Project manager allocates issues based on | ||||
|   theme(s) for sprint, then reviews with team. Each team member | ||||
|   should have roughly two weeks of work allocated (to allow time | ||||
|   in the third week for testing of work completed.) | ||||
|   * Project manager should also sketch out subsequent sprint so | ||||
|     that team may begin work for that sprint during the | ||||
|     third week, since testing and blocker resolution is unlikely | ||||
|     to require all available resources. | ||||
| * __Tag-up.__ Check in and status update among development team. | ||||
|   May amend plan for sprint as-needed. | ||||
| * __Code freeze.__ Any new work from this sprint | ||||
|   (features, bug fixes, enhancements) must be integrated by the | ||||
|   end of the second week of the sprint. After code freeze | ||||
|   (and until the end of the sprint) the only changes that should be | ||||
|   merged into the master branch should directly address issues | ||||
|   needed to pass acceptance testing. | ||||
| * [__Per-release Testing.__](testing/plan.md#per-release-testing) | ||||
|   Structured testing with predefined | ||||
|   success criteria. No release should ship without passing | ||||
|   acceptance tests. Time is allocated in each sprint for subsequent | ||||
|   rounds of acceptance testing if issues are identified during a | ||||
|   prior round. Specific details of acceptance testing need to be | ||||
|   agreed-upon with relevant stakeholders and delivery recipients, | ||||
|   and should be flexible enough to allow changes to plans | ||||
|   (e.g. deferring delivery of some feature in order to ensure | ||||
|   stability of other features.) Baseline testing includes: | ||||
|   * [__Testathon.__](testing/plan.md#user-testing) | ||||
|     Multi-user testing, involving as many users as | ||||
|     is feasible, plus development team. Open-ended; should verify | ||||
|     completed work from this sprint, test exploratorily for | ||||
|     regressions, et cetera. | ||||
|   * [__Long-Duration Test.__](testing/plan.md#long-duration-testing) A | ||||
|     test to verify that the software remains | ||||
|     stable after running for longer durations. May include some | ||||
|     combination of automated testing and user verification (e.g. | ||||
|     checking to verify that software remains subjectively | ||||
|     responsive at conclusion of test.) | ||||
|   * [__Unit Testing.__](testing/plan.md#unit-testing) | ||||
|     Automated testing integrated into the | ||||
|     build. (These tests are verified to pass more often than once | ||||
|     per sprint, as they run before any merge to master, but still | ||||
|     play an important role in per-release testing.) | ||||
| * [__Per-sprint Testing.__](testing/plan.md#per-sprint-testing) | ||||
|   Subset of Pre-release Testing | ||||
|   which should be performed before shipping at the end of any | ||||
|   sprint. Time is allocated for a second round of | ||||
|   Pre-release Testing if the first round is not passed. | ||||
| * __Triage.__ Team reviews issues from acceptance testing and uses | ||||
|   success criteria to determine whether or not they should block | ||||
|   release, then formulates a plan to address these issues before | ||||
|   the next round of acceptance testing. Focus here should be on | ||||
|   ensuring software passes that testing in order to ship on time; | ||||
|   may prefer to disable malfunctioning components and fix them | ||||
|   in a subsequent sprint, for example. | ||||
| * __Ship.__ Tag a code snapshot that has passed acceptance | ||||
|   testing and deploy that version. (Only true if acceptance | ||||
|   testing has passed by this point; if acceptance testing has not | ||||
|   been passed, will need to make ad hoc decisions with stakeholders, | ||||
|   e.g. "extend the sprint" or "defer shipment until end of next | ||||
|   sprint.") | ||||
|  | ||||
|  | ||||
| @@ -1,156 +1,13 @@ | ||||
| # Development Cycle | ||||
|  | ||||
| Development of Open MCT Web occurs on an iterative cycle of | ||||
| sprints and releases. | ||||
|  | ||||
| * A _sprint_ is three weeks in duration, and represents a | ||||
|   set of improvements that can be completed and tested by the | ||||
|   development team. Software at the end of the sprint is | ||||
|   "semi-stable"; it will have undergone reduced testing and may carry | ||||
|   defects or usability issues of lower severity, particularly if | ||||
|   there are workarounds. | ||||
| * A _release_ occurs every four sprints. Releases are stable, and | ||||
|   will have undergone full acceptance testing to ensure that the | ||||
|   software behaves correctly and usably. | ||||
|  | ||||
| ## Roles | ||||
|  | ||||
| The sprint process assumes the presence of a __project manager.__ | ||||
| The project manager is responsible for | ||||
| making tactical decisions about what development work will be | ||||
| performed, and for coordinating with stakeholders to arrive at | ||||
| higher-level strategic decisions about desired functionality | ||||
| and characteristics of the software, major external milestones, | ||||
| and so forth. | ||||
|  | ||||
| In the absence of a dedicated project manager, this role may be rotated | ||||
| among members of the development team on a per-sprint basis. | ||||
|  | ||||
| Responsibilities of the project manager including: | ||||
|  | ||||
| * Maintaining (with agreement of stakeholders) a "road map" of work | ||||
|   planned for future releases/sprints; this should be higher-level, | ||||
|   usually expressed as "themes", | ||||
|   with just enough specificity to gauge feasibility of plans, | ||||
|   relate work back to milestones, and identify longer-term | ||||
|   dependencies. | ||||
| * Determining (with assistance from the rest of the team) which | ||||
|   issues to work on in a given sprint and how they shall be | ||||
|   assigned. | ||||
| * Pre-planning subsequent sprints to ensure that all members of the | ||||
|   team always have a clear direction. | ||||
| * Scheduling and/or ensuring adherence to | ||||
|   [process points](#process-points). | ||||
| * Responding to changes within the sprint (shifting priorities, | ||||
|   new issues) and re-allocating work for the sprint as needed. | ||||
|  | ||||
| ## Sprint Calendar | ||||
|  | ||||
| Certain [process points](#process-points) are regularly scheduled in | ||||
| the sprint cycle. | ||||
|  | ||||
| ### Sprints by Release | ||||
|  | ||||
| Allocation of work among sprints should be planned relative to release | ||||
| goals and milestones. As a general guideline, higher-risk work (large | ||||
| new features which may carry new defects, major refactoring, design | ||||
| changes with uncertain effects on usability) should be allocated to | ||||
| earlier sprints, allowing for time in later sprints to ensure stability. | ||||
|  | ||||
| | Sprint | Focus                                                   | | ||||
| |:------:|:--------------------------------------------------------| | ||||
| | __1__  | Prototyping, design, experimentation.                   | | ||||
| | __2__  | New features, refinements, enhancements.                | | ||||
| | __3__  | Feature completion, low-risk enhancements, bug fixing.  | | ||||
| | __4__  | Stability & quality assurance.                          | | ||||
|  | ||||
| ### Sprints 1-3 | ||||
|  | ||||
| The first three sprints of a release are primarily centered around | ||||
| development work, with regular acceptance testing in the third | ||||
| week. During this third week, the top priority should be passing | ||||
| acceptance testing (e.g. by resolving any blockers found); any | ||||
| resources not needed for this effort should be used to begin work | ||||
| for the subsequent sprint. | ||||
|  | ||||
| | Week  | Mon                       | Tue    | Wed | Thu                          | Fri         | | ||||
| |:-----:|:-------------------------:|:------:|:---:|:----------------------------:|:-----------:| | ||||
| | __1__ | Sprint plan               | Tag-up |     |                              |             | | ||||
| | __2__ |                           | Tag-up |     |                              | Code freeze | | ||||
| | __3__ | Sprint acceptance testing | Triage |     | _Sprint acceptance testing*_ | Ship        | | ||||
|  | ||||
| * If necessary. | ||||
|  | ||||
| ### Sprint 4 | ||||
|  | ||||
| The software must be stable at the end of the fourth sprint; because of | ||||
| this, the fourth sprint is scheduled differently, with a heightened | ||||
| emphasis on testing. | ||||
|  | ||||
| | Week   | Mon                       | Tue    | Wed | Thu                          | Fri         | | ||||
| |-------:|:-------------------------:|:------:|:---:|:----------------------------:|:-----------:| | ||||
| | __1__  | Sprint plan               | Tag-up |     |                              | Code freeze | | ||||
| | __2__  | Acceptance testing        | Triage |     |                              |             | | ||||
| | __3__  | _Acceptance testing*_     | Triage |     | _Acceptance testing*_        | Ship        | | ||||
|  | ||||
| * If necessary. | ||||
|  | ||||
| ## Process Points | ||||
|  | ||||
| * __Sprint plan.__ Project manager allocates issues based on | ||||
|   theme(s) for sprint, then reviews with team. Each team member | ||||
|   should have roughly two weeks of work allocated (to allow time | ||||
|   in the third week for testing of work completed.) | ||||
|   * Project manager should also sketch out subsequent sprint so | ||||
|     that team may begin work for that sprint during the | ||||
|     third week, since testing and blocker resolution is unlikely | ||||
|     to require all available resources. | ||||
| * __Tag-up.__ Check in and status update among development team. | ||||
|   May amend plan for sprint as-needed. | ||||
| * __Code freeze.__ Any new work from this sprint | ||||
|   (features, bug fixes, enhancements) must be integrated by the | ||||
|   end of the second week of the sprint. After code freeze | ||||
|   (and until the end of the sprint) the only changes that should be | ||||
|   merged into the master branch should directly address issues | ||||
|   needed to pass acceptance testing. | ||||
| * __Acceptance Testing.__ Structured testing with predefined | ||||
|   success criteria. No release should ship without passing | ||||
|   acceptance tests. Time is allocated in each sprint for subsequent | ||||
|   rounds of acceptance testing if issues are identified during a | ||||
|   prior round. Specific details of acceptance testing need to be | ||||
|   agreed-upon with relevant stakeholders and delivery recipients, | ||||
|   and should be flexible enough to allow changes to plans | ||||
|   (e.g. deferring delivery of some feature in order to ensure | ||||
|   stability of other features.) Baseline testing includes: | ||||
|   * __Testathon.__ Multi-user testing, involving as many users as | ||||
|     is feasible, plus development team. Open-ended; should verify | ||||
|     completed work from this sprint, test exploratorily for | ||||
|     regressions, et cetera. | ||||
|   * __24-Hour Test.__ A test to verify that the software remains | ||||
|     stable after running for longer durations. May include some | ||||
|     combination of automated testing and user verification (e.g. | ||||
|     checking to verify that software remains subjectively | ||||
|     responsive at conclusion of test.) | ||||
|   * __Automated Testing.__ Automated testing integrated into the | ||||
|     build. (These tests are verified to pass more often than once | ||||
|     per sprint, as they run before any merge to master, but still | ||||
|     play an important role in acceptance testing.) | ||||
| * __Sprint Acceptance Testing.__ Subset of Acceptance Testing | ||||
|   which should be performed before shipping at the end of any | ||||
|   sprint. Time is allocated for a second round of | ||||
|   Sprint Acceptance Testing if the first round is not passed. | ||||
| * __Triage.__ Team reviews issues from acceptance testing and uses | ||||
|   success criteria to determine whether or not they should block | ||||
|   release, then formulates a plan to address these issues before | ||||
|   the next round of acceptance testing. Focus here should be on | ||||
|   ensuring software passes that testing in order to ship on time; | ||||
|   may prefer to disable malfunctioning components and fix them | ||||
|   in a subsequent sprint, for example. | ||||
| * __Ship.__ Tag a code snapshot that has passed acceptance | ||||
|   testing and deploy that version. (Only true if acceptance | ||||
|   testing has passed by this point; if acceptance testing has not | ||||
|   been passed, will need to make ad hoc decisions with stakeholders, | ||||
|   e.g. "extend the sprint" or "defer shipment until end of next | ||||
|   sprint.") | ||||
| # Development Process | ||||
|  | ||||
| The process used to develop Open MCT Web is described in the following | ||||
| documents: | ||||
|  | ||||
| * [Development Cycle](cycle.md): Describes how and when specific | ||||
|   process points are repeated during development. | ||||
| * Testing is described in two documents: | ||||
|   * The [Test Plan](testing/plan.md) summarizes the approaches used | ||||
|     to test Open MCT Web. | ||||
|   * The [Test Procedures](testing/procedures.md) document what | ||||
|     specific tests are performed to verify correctness, and how | ||||
|     they should be carried out. | ||||
|   | ||||
							
								
								
									
										127
									
								
								docs/src/process/testing/plan.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										127
									
								
								docs/src/process/testing/plan.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,127 @@ | ||||
| # Test Plan | ||||
|  | ||||
| ## Test Levels | ||||
|  | ||||
| Testing for Open MCT Web includes: | ||||
|  | ||||
| * _Smoke testing_: Brief, informal testing to verify that no major issues | ||||
|   or regressions are present in the software, or in specific features of | ||||
|   the software. | ||||
| * _Unit testing_: Automated verification of the performance of individual | ||||
|   software components. | ||||
| * _User testing_: Testing with a representative user base to verify | ||||
|   that application behaves usably and as specified. | ||||
| * _Long-duration testing_: Testing which takes place over a long period | ||||
|   of time to detect issues which are not readily noticeable during | ||||
|   shorter test periods. | ||||
|  | ||||
| ### Smoke Testing | ||||
|  | ||||
| Manual, non-rigorous testing of the software and/or specific features | ||||
| of interest. Verifies that the software runs and that basic functionality | ||||
| is present. | ||||
|  | ||||
| ### Unit Testing | ||||
|  | ||||
| Unit tests are automated tests which exercise individual software | ||||
| components. Tests are subject to code review along with the actual | ||||
| implementation, to ensure that tests are applicable and useful. | ||||
|  | ||||
| Unit tests should meet | ||||
| [test standards](https://github.com/nasa/openmctweb/blob/master/CONTRIBUTING.md#test-standards) | ||||
| as described in the contributing guide. | ||||
|  | ||||
| ### User Testing | ||||
|  | ||||
| User testing is performed at scheduled times involving target users | ||||
| of the software or reasonable representatives, along with members of | ||||
| the development team exercising known use cases. Users test the | ||||
| software directly; the software should be configured as similarly to | ||||
| its planned production configuration as is feasible without introducing | ||||
| other risks (e.g. damage to data in a production instance.) | ||||
|  | ||||
| User testing will focus on the following activities: | ||||
|  | ||||
| * Verifying issues resolved since the last test session. | ||||
| * Checking for regressions in areas related to recent changes. | ||||
| * Using major or important features of the software, | ||||
|   as determined by the user. | ||||
| * General "trying to break things." | ||||
|  | ||||
| During user testing, users will | ||||
| [report issues](https://github.com/nasa/openmctweb/blob/master/CONTRIBUTING.md#issue-reporting) | ||||
| as they are encountered. | ||||
|  | ||||
| Desired outcomes of user testing are: | ||||
|  | ||||
| * Identified software defects. | ||||
| * Areas for usability improvement. | ||||
| * Feature requests (particularly missed requirements.) | ||||
| * Recorded issue verification. | ||||
|  | ||||
| ### Long-duration Testing | ||||
|  | ||||
| Long-duration testing occurs over a twenty-four hour period. The | ||||
| software is run in one or more stressing cases representative of expected | ||||
| usage. After twenty-four hours, the software is evaluated for: | ||||
|  | ||||
| * Performance metrics: Have memory usage or CPU utilization increased | ||||
|   during this time period in unexpected or undesirable ways? | ||||
| * Subjective usability: Does the software behave in the same way it did | ||||
|   at the start of the test? Is it as responsive? | ||||
|  | ||||
| Any defects or unexpected behavior identified during testing should be | ||||
| [reported as issues](https://github.com/nasa/openmctweb/blob/master/CONTRIBUTING.md#issue-reporting) | ||||
| and reviewed for severity. | ||||
|  | ||||
| ## Test Performance | ||||
|  | ||||
| Tests are performed at various levels of frequency. | ||||
|  | ||||
| * _Per-merge_: Performed before any new changes are integrated into | ||||
|   the software. | ||||
| * _Per-sprint_: Performed at the end of every [sprint](../cycle.md). | ||||
| * _Per-release_: Performed at the end of every [release](../cycle.md). | ||||
|  | ||||
| ### Per-merge Testing | ||||
|  | ||||
| Before changes are merged, the author of the changes must perform: | ||||
|  | ||||
| * _Smoke testing_ (both generally, and for areas which interact with | ||||
|   the new changes.) | ||||
| * _Unit testing_ (as part of the automated build step.) | ||||
|  | ||||
| Changes are not merged until the author has affirmed that both | ||||
| forms of testing have been performed successfully; this is documented | ||||
| by the [Author Checklist](https://github.com/nasa/openmctweb/blob/master/CONTRIBUTING.md#author-checklist). | ||||
|  | ||||
| ### Per-sprint Testing | ||||
|  | ||||
| Before a sprint is closed, the development team must additionally | ||||
| perform: | ||||
|  | ||||
| * A relevant subset of [_user testing_](procedures.md#user-test-procedures) | ||||
|   identified by the acting [project manager](../cycle.md#roles). | ||||
| * [_Long-duration testing_](procedures.md#long-duration-testng) | ||||
|   (specifically, for 24 hours.) | ||||
|  | ||||
| Issues are reported as a product of both forms of testing. | ||||
|  | ||||
| A sprint is not closed until both categories have been performed on | ||||
| the latest snapshot of the software, _and_ no issues labelled as | ||||
| ["blocker"](https://github.com/nasa/openmctweb/blob/master/CONTRIBUTING.md#issue-reporting) | ||||
| remain open. | ||||
|  | ||||
| ### Per-release Testing | ||||
|  | ||||
| As [per-sprint testing](#per-sprint-testing), except that _user testing_ | ||||
| should cover all test cases, with less focus on changes from the specific | ||||
| sprint or release. | ||||
|  | ||||
| Per-release testing should also include any acceptance testing steps | ||||
| agreed upon with recipients of the software. | ||||
|  | ||||
| A release is not closed until both categories have been performed on | ||||
| the latest snapshot of the software, _and_ no issues labelled as | ||||
| ["blocker" or "critical"](https://github.com/nasa/openmctweb/blob/master/CONTRIBUTING.md#issue-reporting) | ||||
| remain open. | ||||
							
								
								
									
										169
									
								
								docs/src/process/testing/procedures.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										169
									
								
								docs/src/process/testing/procedures.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,169 @@ | ||||
| # Test Procedures | ||||
|  | ||||
| ## Introduction | ||||
|  | ||||
| This document is intended to be used: | ||||
|  | ||||
| * By testers, to verify that Open MCT Web behaves as specified. | ||||
| * By the development team, to document new test cases and to provide | ||||
|   guidance on how to author these. | ||||
|  | ||||
| ## Writing Procedures | ||||
|  | ||||
| ### Template | ||||
|  | ||||
| Procedures for individual tests should use the following template, | ||||
| adapted from [https://swehb.nasa.gov/display/7150/SWE-114](). | ||||
|  | ||||
| Property       | Value | ||||
| ---------------|--------------------------------------------------------------- | ||||
| Test ID        | | ||||
| Relevant reqs. | | ||||
| Prerequisites  | | ||||
| Test input     | | ||||
| Instructions   | | ||||
| Expectation    | | ||||
| Eval. criteria | | ||||
|  | ||||
| For multi-line descriptions, use an asterisk or similar indicator to refer | ||||
| to a longer-form description below. | ||||
|  | ||||
| #### Example Procedure - Edit a Layout | ||||
|  | ||||
| Property       | Value | ||||
| ---------------|--------------------------------------------------------------- | ||||
| Test ID        | MCT-TEST-000X - Edit a layout | ||||
| Relevant reqs. | MCT-EDIT-000Y | ||||
| Prerequisites  | Create a layout, as in MCT-TEST-000Z | ||||
| Test input     | Domain object database XYZ | ||||
| Instructions   | See below * | ||||
| Expectation    | Change to editing context † | ||||
| Eval. criteria | Visual inspection | ||||
|  | ||||
| * Follow the following steps: | ||||
|  | ||||
| 1. Verify that the created layout is currently navigated-to, | ||||
|    as in MCT-TEST-00ZZ. | ||||
| 2. Click the Edit button, identified by a pencil icon and the text "Edit" | ||||
|    displayed on hover. | ||||
|  | ||||
| † Right-hand viewing area should be surrounded by a dashed | ||||
| blue border when a domain object is being edited. | ||||
|  | ||||
| ### Guidelines | ||||
|  | ||||
| Test procedures should be written assuming minimal prior knowledge of the | ||||
| application: Non-standard terms should only be used when they are documented | ||||
| in [the glossary](#glossary), and shorthands used for user actions should | ||||
| be accompanied by useful references to test procedures describing those | ||||
| actions (when available) or descriptions in user documentation. | ||||
|  | ||||
| Test cases should be narrow in scope; if a list of steps is excessively | ||||
| long (or must be written vaguely to be kept short) it should be broken | ||||
| down into multiple tests which reference one another. | ||||
|  | ||||
| All requirements satisfied by Open MCT Web should be verifiable using | ||||
| one or more test procedures. | ||||
|  | ||||
| ## Glossary | ||||
|  | ||||
| This section will contain terms used in test procedures. This may link to | ||||
| a common glossary, to avoid replication of content. | ||||
|  | ||||
| ## Procedures | ||||
|  | ||||
| This section will contain specific test procedures. Presently, procedures | ||||
| are placeholders describing general patterns for setting up and conducting | ||||
| testing. | ||||
|  | ||||
| ### User Testing Setup | ||||
|  | ||||
| These procedures describes a general pattern for setting up for user | ||||
| testing. Specific deployments should customize this pattern with | ||||
| relevant data and any additional steps necessary. | ||||
|  | ||||
| Property       | Value | ||||
| ---------------|--------------------------------------------------------------- | ||||
| Test ID        | MCT-TEST-SETUP0 - User Testing Setup | ||||
| Relevant reqs. | TBD | ||||
| Prerequisites  | Build of relevant components | ||||
| Test input     | Exemplary database; exemplary telemetry data set | ||||
| Instructions   | See below | ||||
| Expectation    | Able to load application in a web browser (Google Chrome) | ||||
| Eval. criteria | Visual inspection | ||||
|  | ||||
| Instructions: | ||||
|  | ||||
| 1. Start telemetry server. | ||||
| 2. Start ElasticSearch. | ||||
| 3. Restore database snapshot to ElasticSearch. | ||||
| 4. Start telemetry playback. | ||||
| 5. Start HTTP server for client sources. | ||||
|  | ||||
| ### User Test Procedures | ||||
|  | ||||
| Specific user test cases have not yet been authored. In their absence, | ||||
| user testing is conducted by: | ||||
|  | ||||
| * Reviewing the text of issues from the issue tracker to understand the | ||||
|   desired behavior, and exercising this behavior in the running application. | ||||
|   (For instance, by following steps to reproduce from the original issue.) | ||||
|   * Issues which appear to be resolved should be marked as such with comments | ||||
|     on the original issue (e.g. "verified during user testing MM/DD/YYYY".) | ||||
|   * Issues which appear not to have been resolved should be reopened with an | ||||
|     explanation of what unexpected behavior has been observed. | ||||
|   * In cases where an issue appears resolved as-worded but other related | ||||
|     undesirable behavior is observed during testing, a new issue should be | ||||
|     opened, and linked to from a comment in the original issues. | ||||
| * General usage of new features and/or existing features which have undergone | ||||
|   recent changes. Defects or problems with usability should be documented | ||||
|   by filing issues in the issue tracker. | ||||
| * Open-ended testing to discover defects, identify usability issues, and | ||||
|   generate feature requests. | ||||
|  | ||||
| ### Long-Duration Testing | ||||
|  | ||||
| The purpose of long-duration testing is to identify performance issues | ||||
| and/or other defects which are sensitive to the amount of time the | ||||
| application is kept running. (Memory leaks, for instance.) | ||||
|  | ||||
| Property       | Value | ||||
| ---------------|--------------------------------------------------------------- | ||||
| Test ID        | MCT-TEST-LDT0 - Long-duration Testing | ||||
| Relevant reqs. | TBD | ||||
| Prerequisites  | MCT-TEST-SETUP0 | ||||
| Test input     | (As for test setup.) | ||||
| Instructions   | See "Instructions" below * | ||||
| Expectation    | See "Expectations" below † | ||||
| Eval. criteria | Visual inspection | ||||
|  | ||||
| * Instructions: | ||||
|  | ||||
| 1. Start `top` or a similar tool to measure CPU usage and memory utilization. | ||||
| 2. Open several user-created displays (as many as would be realistically | ||||
|    opened during actual usage in a stressing case) in some combination of | ||||
|    separate tabs and windows (approximately as many tabs-per-window as | ||||
|    total windows.) | ||||
| 3. Ensure that playback data is set to run continuously for at least 24 hours | ||||
|    (e.g. on a loop.) | ||||
| 4. Record CPU usage and memory utilization. | ||||
| 5. In at least one tab, try some general user interface gestures and make | ||||
|    notes about the subjective experience of using the application. (Particularly, | ||||
|    the degree of responsiveness.) | ||||
| 6. Leave client displays open for 24 hours. | ||||
| 7. Record CPU usage and memory utilization again. | ||||
| 8. Make additional notes about the subjective experience of using the | ||||
|    application (again, particularly responsiveness.) | ||||
| 9. Check logs for any unexpected warnings or errors. | ||||
|  | ||||
| † Expectations: | ||||
|  | ||||
| * At the end of the test, CPU usage and memory usage should both be similar | ||||
|   to their levels at the start of the test. | ||||
| * At the end of the test, subjective usage of the application should not | ||||
|   be observably different from the way it was at the start of the test. | ||||
|   (In particular, responsiveness should not decrease.) | ||||
| * Logs should not contain any unexpected warnings or errors ("expected" | ||||
|   warnings or errors are those that have been documented and prioritized | ||||
|   as known issues, or those that are explained by transient conditions | ||||
|   external to the software, such as network outages.) | ||||
| @@ -1697,8 +1697,7 @@ Next, we utilize this functionality from the template: | ||||
|         </div> | ||||
|     </div> | ||||
| </div> | ||||
| ''' | ||||
|  | ||||
| ``` | ||||
| __tutorials/bargraph/res/templates/bargraph.html__ | ||||
|  | ||||
| Here, we utilize the functions we just provided from the controller to position  | ||||
|   | ||||
| @@ -39,8 +39,11 @@ define( | ||||
|                 start = Date.now(); | ||||
|  | ||||
|             function update() { | ||||
|                 var secs = (Date.now() - start) / 1000; | ||||
|                 var now = Date.now(), | ||||
|                     secs = (now - start) / 1000; | ||||
|                 displayed = Math.round(digests / secs); | ||||
|                 start = now; | ||||
|                 digests = 0; | ||||
|             } | ||||
|  | ||||
|             function increment() { | ||||
|   | ||||
| @@ -31,7 +31,7 @@ | ||||
|     "jshint": "jshint platform example || exit 0", | ||||
|     "watch": "karma start", | ||||
|     "jsdoc": "jsdoc -c jsdoc.json -r -d target/docs/api", | ||||
|     "otherdoc": "node docs/gendocs.js --in docs/src --out target/docs", | ||||
|     "otherdoc": "node docs/gendocs.js --in docs/src --out target/docs --suppress-toc 'docs/src/index.md|docs/src/process/index.md'", | ||||
|     "docs": "npm run jsdoc ; npm run otherdoc" | ||||
|   }, | ||||
|   "repository": { | ||||
|   | ||||
| @@ -102,6 +102,12 @@ | ||||
|                 "implementation": "navigation/NavigationService.js" | ||||
|             } | ||||
|         ], | ||||
|         "policies": [ | ||||
|             { | ||||
|                 "implementation": "creation/CreationPolicy.js", | ||||
|                 "category": "creation" | ||||
|             } | ||||
|         ], | ||||
|         "actions": [ | ||||
|             { | ||||
|                 "key": "navigate", | ||||
|   | ||||
| @@ -69,7 +69,7 @@ define( | ||||
|  | ||||
|             // Introduce one create action per type | ||||
|             return this.typeService.listTypes().filter(function (type) { | ||||
|                 return type.hasFeature("creation"); | ||||
|                 return self.policyService.allow("creation", type); | ||||
|             }).map(function (type) { | ||||
|                 return new CreateAction( | ||||
|                     type, | ||||
|   | ||||
							
								
								
									
										45
									
								
								platform/commonUI/browse/src/creation/CreationPolicy.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								platform/commonUI/browse/src/creation/CreationPolicy.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,45 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT Web, Copyright (c) 2014-2015, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
|  * Administration. All rights reserved. | ||||
|  * | ||||
|  * Open MCT Web 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 Web 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. | ||||
|  *****************************************************************************/ | ||||
| /*global define*/ | ||||
|  | ||||
| define( | ||||
|     [], | ||||
|     function () { | ||||
|         "use strict"; | ||||
|  | ||||
|         /** | ||||
|          * A policy for determining whether objects of a given type can be | ||||
|          * created. | ||||
|          * @constructor | ||||
|          * @implements {Policy} | ||||
|          * @memberof platform/commonUI/browse | ||||
|          */ | ||||
|         function CreationPolicy() { | ||||
|         } | ||||
|  | ||||
|         CreationPolicy.prototype.allow = function (type) { | ||||
|             return type.hasFeature("creation"); | ||||
|         }; | ||||
|  | ||||
|         return CreationPolicy; | ||||
|     } | ||||
| ); | ||||
| @@ -33,6 +33,9 @@ define( | ||||
|             var mockTypeService, | ||||
|                 mockDialogService, | ||||
|                 mockCreationService, | ||||
|                 mockPolicyService, | ||||
|                 mockCreationPolicy, | ||||
|                 mockPolicyMap = {}, | ||||
|                 mockTypes, | ||||
|                 provider; | ||||
|  | ||||
| @@ -67,14 +70,32 @@ define( | ||||
|                     "creationService", | ||||
|                     [ "createObject" ] | ||||
|                 ); | ||||
|                 mockPolicyService = jasmine.createSpyObj( | ||||
|                     "policyService", | ||||
|                     [ "allow" ] | ||||
|                 ); | ||||
|  | ||||
|                 mockTypes = [ "A", "B", "C" ].map(createMockType); | ||||
|  | ||||
|                 mockTypes.forEach(function(type){ | ||||
|                     mockPolicyMap[type.getName()] = true; | ||||
|                 }); | ||||
|  | ||||
|                 mockCreationPolicy = function(type){ | ||||
|                     return mockPolicyMap[type.getName()]; | ||||
|                 }; | ||||
|  | ||||
|                 mockPolicyService.allow.andCallFake(function(category, type){ | ||||
|                     return category === "creation" && mockCreationPolicy(type) ? true : false; | ||||
|                 }); | ||||
|  | ||||
|                 mockTypeService.listTypes.andReturn(mockTypes); | ||||
|  | ||||
|                 provider = new CreateActionProvider( | ||||
|                     mockTypeService, | ||||
|                     mockDialogService, | ||||
|                     mockCreationService | ||||
|                     mockCreationService, | ||||
|                     mockPolicyService | ||||
|                 ); | ||||
|             }); | ||||
|  | ||||
| @@ -94,15 +115,15 @@ define( | ||||
|  | ||||
|             it("does not expose non-creatable types", function () { | ||||
|                 // One of the types won't have the creation feature... | ||||
|                 mockTypes[1].hasFeature.andReturn(false); | ||||
|                 mockPolicyMap[mockTypes[0].getName()] = false; | ||||
|                 // ...so it should have been filtered out. | ||||
|                 expect(provider.getActions({ | ||||
|                     key: "create", | ||||
|                     domainObject: {} | ||||
|                 }).length).toEqual(2); | ||||
|                 // Make sure it was creation which was used to check | ||||
|                 expect(mockTypes[1].hasFeature) | ||||
|                     .toHaveBeenCalledWith("creation"); | ||||
|                 expect(mockPolicyService.allow) | ||||
|                     .toHaveBeenCalledWith("creation", mockTypes[0]); | ||||
|             }); | ||||
|         }); | ||||
|     } | ||||
|   | ||||
							
								
								
									
										53
									
								
								platform/commonUI/browse/test/creation/CreationPolicySpec.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								platform/commonUI/browse/test/creation/CreationPolicySpec.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,53 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT Web, Copyright (c) 2014-2015, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
|  * Administration. All rights reserved. | ||||
|  * | ||||
|  * Open MCT Web 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 Web 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. | ||||
|  *****************************************************************************/ | ||||
| /*global define,describe,it,expect,beforeEach,jasmine*/ | ||||
|  | ||||
| define( | ||||
|     ["../../src/creation/CreationPolicy"], | ||||
|     function (CreationPolicy) { | ||||
|         "use strict"; | ||||
|  | ||||
|         describe("The creation policy", function () { | ||||
|             var mockType, | ||||
|                 policy; | ||||
|  | ||||
|             beforeEach(function () { | ||||
|                 mockType = jasmine.createSpyObj( | ||||
|                     'type', | ||||
|                     ['hasFeature'] | ||||
|                 ); | ||||
|  | ||||
|                 policy = new CreationPolicy(); | ||||
|             }); | ||||
|  | ||||
|             it("allows creation of types with the creation feature", function () { | ||||
|                 mockType.hasFeature.andReturn(true); | ||||
|                 expect(policy.allow(mockType)).toBeTruthy(); | ||||
|             }); | ||||
|  | ||||
|             it("disallows creation of types without the creation feature", function () { | ||||
|                 mockType.hasFeature.andReturn(false); | ||||
|                 expect(policy.allow(mockType)).toBeFalsy(); | ||||
|             }); | ||||
|         }); | ||||
|     } | ||||
| ); | ||||
| @@ -8,6 +8,7 @@ | ||||
|     "creation/CreateMenuController", | ||||
|     "creation/CreateWizard", | ||||
|     "creation/CreationService", | ||||
|     "creation/CreationPolicy", | ||||
|     "creation/LocatorController", | ||||
|     "navigation/NavigateAction", | ||||
|     "navigation/NavigationService", | ||||
|   | ||||
| @@ -24,7 +24,9 @@ | ||||
|     <ul class="tree"> | ||||
|         <li ng-repeat="containedObject in composition"> | ||||
|             <span class="tree-item"> | ||||
|                 <mct-representation key="'label'" mct-object="containedObject"> | ||||
|                 <mct-representation key="'label'" | ||||
|                                     mct-object="containedObject" | ||||
|                                     class="rep-object-label"> | ||||
|                 </mct-representation> | ||||
|             </span> | ||||
|         </li> | ||||
|   | ||||
| @@ -50,7 +50,7 @@ define( | ||||
|             // Simply trigger refresh of in-view objects; do not | ||||
|             // write anything to database. | ||||
|             persistence.persist = function () { | ||||
|                 cache.markDirty(editableObject); | ||||
|                 return cache.markDirty(editableObject); | ||||
|             }; | ||||
|  | ||||
|             // Delegate refresh to the original object; this avoids refreshing | ||||
|   | ||||
| @@ -111,6 +111,7 @@ define( | ||||
|          */ | ||||
|         EditableDomainObjectCache.prototype.markDirty = function (domainObject) { | ||||
|             this.dirtyObjects[domainObject.getId()] = domainObject; | ||||
|             return this.$q.when(true); | ||||
|         }; | ||||
|  | ||||
|         /** | ||||
|   | ||||
| @@ -31,6 +31,7 @@ define( | ||||
|                 mockEditableObject, | ||||
|                 mockDomainObject, | ||||
|                 mockCache, | ||||
|                 mockPromise, | ||||
|                 capability; | ||||
|  | ||||
|             beforeEach(function () { | ||||
| @@ -50,7 +51,9 @@ define( | ||||
|                     "cache", | ||||
|                     [ "markDirty" ] | ||||
|                 ); | ||||
|                 mockPromise = jasmine.createSpyObj("promise", ["then"]); | ||||
|  | ||||
|                 mockCache.markDirty.andReturn(mockPromise); | ||||
|                 mockDomainObject.getCapability.andReturn(mockPersistence); | ||||
|  | ||||
|                 capability = new EditablePersistenceCapability( | ||||
| @@ -84,6 +87,10 @@ define( | ||||
|                 expect(mockPersistence.refresh).toHaveBeenCalled(); | ||||
|             }); | ||||
|  | ||||
|             it("returns a promise from persist", function () { | ||||
|                 expect(capability.persist().then).toEqual(jasmine.any(Function)); | ||||
|             }); | ||||
|  | ||||
|         }); | ||||
|     } | ||||
| ); | ||||
| @@ -19,6 +19,10 @@ | ||||
|             { | ||||
|                 "implementation": "StyleSheetLoader.js", | ||||
|                 "depends": [ "stylesheets[]", "$document", "THEME" ] | ||||
|             }, | ||||
|             { | ||||
|                 "implementation": "UnsupportedBrowserWarning.js", | ||||
|                 "depends": [ "notificationService", "agentService" ] | ||||
|             } | ||||
|         ], | ||||
|         "stylesheets": [ | ||||
| @@ -225,10 +229,6 @@ | ||||
|                 "templateUrl": "templates/subtree.html", | ||||
|                 "uses": [ "composition" ] | ||||
|             }, | ||||
|             { | ||||
|                 "key": "test", | ||||
|                 "templateUrl": "templates/test.html" | ||||
|             }, | ||||
|             { | ||||
|                 "key": "tree-node", | ||||
|                 "templateUrl": "templates/tree-node.html", | ||||
|   | ||||
| @@ -35,24 +35,23 @@ | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
|  | ||||
|  | ||||
| 	.l-autoflow-header { | ||||
| 		bottom: auto; | ||||
| 		height: $headerH; | ||||
| 		line-height: $headerH; | ||||
| 		min-width: $colW; | ||||
| 		span { | ||||
| 			vertical-align: middle; | ||||
| 		} | ||||
|         min-width: $colW; | ||||
|         .t-last-update { | ||||
|             overflow: hidden; | ||||
|         } | ||||
| 		.s-btn.change-column-width { | ||||
| 			@include trans-prop-nice-fade(500ms); | ||||
| 			opacity: 0; | ||||
| 		} | ||||
| 		.l-filter { | ||||
| 			margin-left: $interiorMargin; | ||||
|             display: block; | ||||
| 			margin-right: $interiorMargin; | ||||
| 			input.t-filter-input { | ||||
| 				width: 100px; | ||||
| 				width: 150px; | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| @@ -127,4 +126,12 @@ | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| .frame { | ||||
|     &.child-frame.panel { | ||||
|         .autoflow .l-autoflow-header .l-filter { | ||||
|             display: none; | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -71,7 +71,7 @@ $itemPadLR: 5px; | ||||
| $treeVCW: 10px; | ||||
| $treeTypeIconH: 1.4em; // was 16px | ||||
| $treeTypeIconHPx: 16px; | ||||
| $treeTypeIconW: 20px; | ||||
| $treeTypeIconW: 18px; | ||||
| $treeContextTriggerW: 20px; | ||||
| // Tabular | ||||
| $tabularHeaderH: 22px; //18px | ||||
|   | ||||
| @@ -31,10 +31,6 @@ a.disabled { | ||||
|     border-bottom: 1px solid rgba(#fff, 0.3); | ||||
| } | ||||
|  | ||||
| .outline { | ||||
|     @include boxOutline(); | ||||
| } | ||||
|  | ||||
| .test-stripes { | ||||
| 	@include bgDiagonalStripes(); | ||||
| } | ||||
|   | ||||
| @@ -73,31 +73,34 @@ | ||||
| } | ||||
|  | ||||
| .l-icon-alert { | ||||
| 	display: none !important; // Remove this when alerts are enabled | ||||
| 	display: none !important; | ||||
| 	&:before { | ||||
| 		color: $colorAlert; | ||||
| 		content: "!"; | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // NEW!! | ||||
| .t-item-icon { | ||||
| 	// Used in grid-item.html, tree-item, inspector location, more? | ||||
| 	@extend .ui-symbol; | ||||
|     @extend .icon; | ||||
|     display: inline-block; | ||||
|     line-height: normal; // This is Ok for the symbolsfont | ||||
|     position: relative; | ||||
|     .t-item-icon-glyph { | ||||
|         position: absolute; | ||||
|     } | ||||
|     &.l-icon-link { | ||||
|         &:before { | ||||
|             color: $colorIconLink; | ||||
|             content: "\f4"; | ||||
|             height: auto; width: auto; | ||||
|             position: absolute; | ||||
|             left: 0; top: 0; right: 0; bottom: 10%; | ||||
|             @include transform-origin(bottom, left); | ||||
|             @include transform(scale(0.3)); | ||||
|             z-index: 2; | ||||
|         .t-item-icon-glyph { | ||||
|             &:before { | ||||
|                 color: $colorIconLink; | ||||
|                 content: "\f4"; | ||||
|                 height: auto; width: auto; | ||||
|                 position: absolute; | ||||
|                 left: 0; top: 0; right: 0; bottom: 10%; | ||||
|                 @include transform-origin(bottom, left); | ||||
|                 @include transform(scale(0.3)); | ||||
|                 z-index: 2; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -84,12 +84,20 @@ | ||||
|     } | ||||
|  | ||||
|     .inspector-location { | ||||
|         //line-height: 180%; | ||||
|         .location-item { | ||||
|             $h: 1.2em; | ||||
|             @include box-sizing(border-box); | ||||
|             cursor: pointer; | ||||
|             display: inline-block; | ||||
|             line-height: $h; | ||||
|             position: relative; | ||||
|             padding: 2px 4px; | ||||
|             .t-object-label { | ||||
|                 .t-item-icon { | ||||
|                     height: $h; | ||||
|                     width: 0.7rem; | ||||
|                 } | ||||
|             } | ||||
|             &:hover { | ||||
|                 background: $colorItemTreeHoverBg; | ||||
|                 color: $colorItemTreeHoverFg; | ||||
| @@ -104,6 +112,7 @@ | ||||
|             display: inline-block; | ||||
|             font-family: symbolsfont; | ||||
|             font-size: 8px; | ||||
|             font-style: normal !important; | ||||
|             line-height: inherit; | ||||
|             margin-left: $interiorMarginSm; | ||||
|             width: 4px; | ||||
|   | ||||
| @@ -60,6 +60,7 @@ | ||||
| @import "overlay/overlay"; | ||||
| @import "mobile/overlay/overlay"; | ||||
| @import "tree/tree"; | ||||
| @import "object-label"; | ||||
| @import "mobile/tree"; | ||||
| @import "user-environ/frame"; | ||||
| @import "user-environ/top-bar"; | ||||
|   | ||||
| @@ -300,7 +300,7 @@ | ||||
| 	@include desktop { | ||||
| 		@if $bgHov != none { | ||||
| 			&:not(.disabled):hover { | ||||
| 				background: $bgHov; | ||||
| 				@include background-image($bgHov); | ||||
| 				>.icon { | ||||
| 					color: lighten($ic, $ltGamma); | ||||
| 				} | ||||
|   | ||||
							
								
								
									
										69
									
								
								platform/commonUI/general/res/sass/_object-label.scss
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										69
									
								
								platform/commonUI/general/res/sass/_object-label.scss
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,69 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT Web, Copyright (c) 2014-2015, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
|  * Administration. All rights reserved. | ||||
|  * | ||||
|  * Open MCT Web 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 Web 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. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| // mct-representation surrounding an object-label key="'label'" | ||||
| .rep-object-label { | ||||
|     @include flex-direction(row); | ||||
|     @include flex(1 1 auto); | ||||
|     height: inherit; | ||||
|     line-height: inherit; | ||||
|     min-width: 0; | ||||
| } | ||||
|  | ||||
| .t-object-label { | ||||
|     .t-item-icon { | ||||
|         margin-right: $interiorMargin; | ||||
|     } | ||||
| } | ||||
|  | ||||
| mct-representation { | ||||
|     &.s-status-pending { | ||||
|         .t-object-label { | ||||
|             .t-item-icon { | ||||
|                 &:before { | ||||
|                     $spinBW: 4px; | ||||
|                     $spinD: 0; | ||||
|                     @include spinner($spinBW); | ||||
|                     content: ""; | ||||
|                     display: block; | ||||
|                     position: absolute; | ||||
|                     left: 50%; | ||||
|                     top: 50%; | ||||
|                     padding: 30%; | ||||
|                     width: $spinD; | ||||
|                     height: $spinD; | ||||
|                 } | ||||
|                 .t-item-icon-glyph { | ||||
|                     display: none; | ||||
|                 } | ||||
|             } | ||||
|             .t-title-label { | ||||
|                 font-style: italic; | ||||
|                 opacity: 0.6; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| .selected mct-representation.s-status-pending .t-object-label .t-item-icon:before { | ||||
|     border-color: rgba($colorItemTreeSelectedFg, 0.25); | ||||
|     border-top-color: rgba($colorItemTreeSelectedFg, 1.0); | ||||
| } | ||||
| @@ -37,6 +37,8 @@ | ||||
| } | ||||
|  | ||||
| .status.block { | ||||
|     $transDelay: 1.5s; | ||||
|     $transSpeed: .25s; | ||||
| 	color: $colorStatusDefault; | ||||
| 	cursor: default; | ||||
| 	display: inline-block; | ||||
| @@ -44,13 +46,47 @@ | ||||
| 	.status-indicator, | ||||
| 	.label, | ||||
| 	.count { | ||||
| 		//@include test(#00ff00); | ||||
| 		display: inline-block; | ||||
| 		vertical-align: top; | ||||
| 	} | ||||
|  | ||||
|     &.no-icon { | ||||
|         .status-indicator { | ||||
|             display: none; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     &.float-right { | ||||
|         float: right; | ||||
|     } | ||||
|  | ||||
|     &.subtle { | ||||
|         opacity: 0.5; | ||||
|     } | ||||
| 	.status-indicator { | ||||
| 		margin-right: $interiorMarginSm; | ||||
| 	} | ||||
|  | ||||
|     &:not(.no-collapse) { | ||||
|         .label { | ||||
|             // Max-width silliness is necessary for width transition | ||||
|             @include trans-prop-nice(max-width, $transSpeed, $transDelay); | ||||
|             overflow: hidden; | ||||
|             max-width: 0px; | ||||
|         } | ||||
|         &:hover { | ||||
|             .label { | ||||
|                 @include trans-prop-nice(max-width, $transSpeed, 0s); | ||||
|                 max-width: 450px; | ||||
|                 width: auto; | ||||
|             } | ||||
|             .count { | ||||
|                 @include trans-prop-nice(max-width, $transSpeed, 0s); | ||||
|                 opacity: 0; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
| 	&.ok .status-indicator, | ||||
|     &.info .status-indicator { | ||||
| 		color: $colorStatusInfo; | ||||
| @@ -63,26 +99,11 @@ | ||||
| 	&.error .status-indicator { | ||||
| 		color: $colorStatusError; | ||||
| 	} | ||||
| 	.label { | ||||
| 		// Max-width silliness is necessary for width transition | ||||
| 		@include trans-prop-nice(max-width, .25s); | ||||
| 		overflow: hidden; | ||||
| 		max-width: 0px; | ||||
| 	} | ||||
| 	.count { | ||||
| 		@include trans-prop-nice(opacity, .25s); | ||||
| 		@include trans-prop-nice(opacity, $transSpeed, $transDelay); | ||||
| 		font-weight: bold; | ||||
| 		opacity: 1; | ||||
| 	} | ||||
| 	&:hover { | ||||
| 		.label { | ||||
| 			max-width: 450px; | ||||
| 			width: auto; | ||||
| 		} | ||||
| 		.count { | ||||
| 			opacity: 0; | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| /* Styles for messages and message banners */ | ||||
|   | ||||
| @@ -24,21 +24,27 @@ | ||||
|     100% { transform: rotate(359deg); } | ||||
| } | ||||
|  | ||||
| @mixin wait-spinner2($b: 5px, $c: $colorAlt1) { | ||||
| @mixin spinner($b: 5px) { | ||||
|     @include keyframes(rotateCentered) { | ||||
|          0%   { transform: translateX(-50%) translateY(-50%) rotate(0deg); } | ||||
|          100% { transform: translateX(-50%) translateY(-50%) rotate(359deg); } | ||||
|      } | ||||
|         0%   { @include transform(translateX(-50%) translateY(-50%) rotate(0deg)); } | ||||
|         100% { @include transform(translateX(-50%) translateY(-50%) rotate(359deg)); } | ||||
|     } | ||||
|     @include animation-name(rotateCentered); | ||||
|     @include animation-duration(0.5s); | ||||
|     @include animation-iteration-count(infinite); | ||||
|     @include animation-timing-function(linear); | ||||
|     @include transform-origin(center); | ||||
|     border-style: solid; | ||||
|     border-width: $b; | ||||
|     @include border-radius(100%); | ||||
| } | ||||
|  | ||||
|  | ||||
| @mixin wait-spinner2($b: 5px, $c: $colorAlt1) { | ||||
|     @include spinner($b); | ||||
|     @include box-sizing(border-box); | ||||
|     border-color: rgba($c, 0.25); | ||||
|     border-top-color: rgba($c, 1.0); | ||||
|     border-style: solid; | ||||
|     border-width: 5px; | ||||
|     @include border-radius(100%); | ||||
|     @include box-sizing(border-box); | ||||
|     display: block; | ||||
|     position: absolute; | ||||
|     height: 0; width: 0; | ||||
|   | ||||
| @@ -31,34 +31,34 @@ $tabletItemH: floor($ueBrowseGridItemLg/3); | ||||
|  | ||||
| /************************** MOBILE TREE MENU DIMENSIONS */ | ||||
| $mobileTreeItemH: 35px; | ||||
| $mobileTreeItemIndent: 20px; | ||||
| $mobileTreeItemIndent: 15px; | ||||
| $mobileTreeRightArrowW: 30px; | ||||
|  | ||||
| /************************** DEVICE WIDTHS */ | ||||
| // IMPORTANT! Usage assumes that ranges are mutually exclusive and have no gaps | ||||
| $phoMaxW: 514px; | ||||
| $tabMinW: 515px; | ||||
| $tabMaxW: 1280px; | ||||
| $desktopMinW: 1281px; | ||||
| $phoMaxW: 767px; | ||||
| $tabMinW: 768px; | ||||
| $tabMaxW: 1024px; | ||||
| $desktopMinW: 1025px; | ||||
|  | ||||
| /************************** MEDIA QUERIES: WINDOW CHECKS FOR SPECIFIC ORIENTATIONS FOR EACH DEVICE */ | ||||
| $screenPortrait: "screen and (orientation: portrait)"; | ||||
| $screenLandscape: "screen and (orientation: landscape)"; | ||||
| $screenPortrait: "(orientation: portrait)"; | ||||
| $screenLandscape: "(orientation: landscape)"; | ||||
|  | ||||
| $mobileDevice: "(max-device-width: #{$tabMaxW})"; | ||||
| //$mobileDevice: "(max-device-width: #{$tabMaxW})"; | ||||
|  | ||||
| $phoneCheck: "(max-device-width: #{$phoMaxW})"; | ||||
| $tabletCheck: $mobileDevice; | ||||
| $desktopCheck: "(min-device-width: #{$desktopMinW})"; | ||||
| $tabletCheck: "(min-device-width: #{$tabMinW}) and (max-device-width: #{$tabMaxW})"; | ||||
| $desktopCheck: "(min-device-width: #{$desktopMinW}) and (-webkit-min-device-pixel-ratio: 1)"; | ||||
|  | ||||
| /************************** MEDIA QUERIES: WINDOWS FOR SPECIFIC ORIENTATIONS FOR EACH DEVICE */ | ||||
| $phonePortrait: "#{$screenPortrait} and #{$phoneCheck} and #{$mobileDevice}"; | ||||
| $phoneLandscape: "#{$screenLandscape} and #{$phoneCheck} and #{$mobileDevice}"; | ||||
| $phonePortrait: "only screen and #{$screenPortrait} and #{$phoneCheck}"; | ||||
| $phoneLandscape: "only screen and #{$screenLandscape} and #{$phoneCheck}"; | ||||
|  | ||||
| $tabletPortrait: "#{$screenPortrait} and #{$tabletCheck} and #{$mobileDevice}"; | ||||
| $tabletLandscape: "#{$screenLandscape} and #{$tabletCheck} and #{$mobileDevice}"; | ||||
| $tabletPortrait: "only screen and #{$screenPortrait} and #{$tabletCheck}"; | ||||
| $tabletLandscape: "only screen and #{$screenLandscape} and #{$tabletCheck}"; | ||||
|  | ||||
| $desktop: "screen and #{$desktopCheck}"; | ||||
| $desktop: "only screen and #{$desktopCheck}"; | ||||
|  | ||||
| /************************** DEVICE PARAMETERS FOR MENUS/REPRESENTATIONS */ | ||||
| $proporMenuOnly: 90%; | ||||
|   | ||||
| @@ -30,25 +30,30 @@ | ||||
|     } | ||||
| 	.tree-item, | ||||
| 	.search-result-item { | ||||
| 		height: $mobileTreeItemH; | ||||
| 		line-height: $mobileTreeItemH; | ||||
| 		margin-bottom: 0px; | ||||
| 		height: $mobileTreeItemH !important; | ||||
| 		line-height: $mobileTreeItemH !important; | ||||
| 		margin-bottom: 0px !important; | ||||
| 		.view-control { | ||||
| 			//@include test(red); | ||||
| 			position: absolute; | ||||
| 			font-size: 1.1em; | ||||
| 			height: $mobileTreeItemH; | ||||
| 			line-height: inherit; | ||||
| 			right: 0px; | ||||
| 			width: $mobileTreeRightArrowW; | ||||
| 			text-align: center; | ||||
|             font-size: 1.2em; | ||||
|             margin-right: 0; | ||||
|             order: 2; | ||||
|             width: $mobileTreeItemH; | ||||
|             &.has-children { | ||||
|                 &:before { | ||||
|                     content: "\7d"; | ||||
|                     left: 50%; | ||||
|                     @include transform(translateX(-50%) rotate(90deg)); | ||||
|                 } | ||||
|                 &.expanded:before { | ||||
|                     @include transform(translateX(-50%) rotate(270deg)); | ||||
|                 } | ||||
|             } | ||||
| 		} | ||||
|  | ||||
| 		.label, | ||||
| 		.t-object-label { | ||||
| 			left: 0; | ||||
| 			right: $mobileTreeRightArrowW + $interiorMargin; // Allows tree item name to stop prior to the arrow | ||||
| 			line-height: inherit; | ||||
|             .t-item-icon.l-icon-link .t-item-icon-glyph:before { | ||||
|                 bottom: 20%; // Shift up due to height of mobile menu items | ||||
|             } | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| @include phone { | ||||
| 	.search { | ||||
| 	.search-holder { | ||||
| 		.search-bar { | ||||
| 			// Hide menu-icon and adjust spacing when in phone mode | ||||
| 			.menu-icon { | ||||
|   | ||||
| @@ -82,6 +82,7 @@ | ||||
|             left: $interiorMarginSm; | ||||
|             @include trans-prop-nice(color, 250ms); | ||||
|             pointer-events: none; | ||||
|             z-index: 1; | ||||
|         } | ||||
|  | ||||
| 		// Make icon lighten when hovering over search bar | ||||
| @@ -127,7 +128,7 @@ | ||||
| 	} | ||||
|  | ||||
| 	.active-filter-display { | ||||
| 		$s: 0.65em; | ||||
| 		$s: 0.7em; | ||||
| 		$p: $interiorMargin; | ||||
| 		@include box-sizing(border-box); | ||||
| 		line-height: 130%; | ||||
| @@ -146,7 +147,6 @@ | ||||
|  | ||||
| 	.search-results { | ||||
|         @include trans-prop-nice((opacity, visibility), 250ms); | ||||
|         margin-top: $interiorMarginLg; // Always include margin here to fend off the search input | ||||
| 		padding-right: $interiorMargin; | ||||
|         .hint { | ||||
|             margin-bottom: $interiorMarginLg; | ||||
|   | ||||
| @@ -35,23 +35,35 @@ ul.tree { | ||||
| .tree-item, | ||||
| .search-result-item { | ||||
| 	$runningItemW: 0; | ||||
|     @extend .l-flex-row; | ||||
| 	@include box-sizing(border-box); | ||||
| 	@include border-radius($basicCr); | ||||
| 	@include single-transition(background-color, 0.25s); | ||||
| 	display: block; | ||||
| 	font-size: 0.8rem; | ||||
| 	height: $menuLineH; | ||||
| 	line-height: $menuLineH; | ||||
| 	margin-bottom: $interiorMarginSm; | ||||
|     padding: 0 $interiorMarginSm; | ||||
| 	position: relative; | ||||
|  | ||||
| 	.view-control { | ||||
| 		color: $colorItemTreeVC; | ||||
| 		display: inline-block; | ||||
| 		margin-left: $interiorMargin; | ||||
| 		font-size: 0.75em; | ||||
|         font-size: 0.75em; | ||||
| 		margin-right: $interiorMargin; | ||||
|         height: 100%; | ||||
|         line-height: inherit; | ||||
| 		width: $treeVCW; | ||||
| 		$runningItemW: $interiorMargin + $treeVCW; | ||||
|         &.has-children { | ||||
|             &:before { | ||||
|                 position: absolute; | ||||
|                 @include trans-prop-nice(transform, 100ms); | ||||
|                 content: "\3e"; | ||||
|                 @include transform-origin(center); | ||||
|             } | ||||
|             &.expanded:before { | ||||
|                 @include transform(rotate(90deg)); | ||||
|             } | ||||
|         } | ||||
| 		@include desktop { | ||||
| 			&:hover { | ||||
| 				color: $colorItemTreeVCHover !important; | ||||
| @@ -59,64 +71,17 @@ ul.tree { | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	.label, | ||||
| 	.t-object-label { | ||||
| 		display: block; | ||||
| 		@include absPosDefault(); | ||||
| 		line-height: $menuLineH; | ||||
|  | ||||
|         .t-item-icon { | ||||
|             @include txtShdwSubtle($shdwItemTreeIcon); | ||||
|             font-size: $treeTypeIconH; | ||||
|             color: $colorItemTreeIcon; | ||||
|             position: absolute; | ||||
|             left: $interiorMargin; | ||||
|             top: 50%; | ||||
|             width: $treeTypeIconH; | ||||
|             @include transform(translateY(-50%)); | ||||
|             width: $treeTypeIconW; | ||||
|         } | ||||
|  | ||||
| 		.type-icon { | ||||
| 			//@include absPosDefault(0, false); | ||||
| 			$d: $treeTypeIconH; | ||||
| 			@include txtShdwSubtle($shdwItemTreeIcon); | ||||
| 			font-size: $treeTypeIconH; | ||||
| 			color: $colorItemTreeIcon; | ||||
| 			left: $interiorMargin; | ||||
| 			position: absolute; | ||||
| 			@include verticalCenterBlock($menuLineHPx, $treeTypeIconHPx); | ||||
| 			line-height: 100%; | ||||
| 			right: auto; width: $treeTypeIconH; | ||||
|  | ||||
| 			.icon { | ||||
| 				&.l-icon-link, | ||||
| 				&.l-icon-alert { | ||||
| 					position: absolute; | ||||
| 					z-index: 2; | ||||
| 				} | ||||
| 				&.l-icon-alert { | ||||
| 					$d: 8px; | ||||
| 					@include ancillaryIcon($d, $colorAlert); | ||||
| 					top: 1px; | ||||
| 					right: -2px; | ||||
| 				} | ||||
| 				&.l-icon-link { | ||||
| 					$d: 8px; | ||||
| 					@include ancillaryIcon($d, $colorIconLink); | ||||
| 					left: -3px; | ||||
| 					bottom: 0px; | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 		.title-label, | ||||
|         .t-title-label { | ||||
| 			@include absPosDefault(); | ||||
| 			display: block; | ||||
| 			left: $runningItemW + ($interiorMargin * 3); | ||||
| 			overflow: hidden; | ||||
| 			text-overflow: ellipsis; | ||||
| 			white-space: nowrap; | ||||
| 		} | ||||
|             @include ellipsize(); | ||||
|         } | ||||
| 	} | ||||
|  | ||||
| 	&.selected { | ||||
| @@ -126,12 +91,11 @@ ul.tree { | ||||
| 			color: $colorItemTreeSelectedVC; | ||||
| 		} | ||||
| 		.t-object-label .t-item-icon { | ||||
| 			color: $colorItemTreeSelectedFg; //$colorItemTreeIconHover; | ||||
| 			color: $colorItemTreeSelectedFg; | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	&:not(.selected) { | ||||
| 		// NOTE: [Mobile] Removed Hover on Mobile | ||||
| 		@include desktop { | ||||
| 			&:hover { | ||||
| 				background: $colorItemTreeHoverBg; | ||||
| @@ -160,8 +124,28 @@ ul.tree { | ||||
| 	} | ||||
| } | ||||
|  | ||||
| .tree-item { | ||||
| 	.t-object-label { | ||||
| 		left: $interiorMargin + $treeVCW; | ||||
| 	} | ||||
| } | ||||
| mct-representation { | ||||
|     &.s-status-pending { | ||||
|         .t-object-label { | ||||
|             .t-item-icon { | ||||
|                 &:before { | ||||
|                     $spinBW: 4px; | ||||
|                     @include spinner($spinBW); | ||||
|                     border-color: rgba($colorItemTreeIcon, 0.25); | ||||
|                     border-top-color: rgba($colorItemTreeIcon, 1.0); | ||||
|                 } | ||||
|                 .t-item-icon-glyph { | ||||
|                     display: none; | ||||
|                 } | ||||
|             } | ||||
|             .t-title-label { | ||||
|                 font-style: italic; | ||||
|                 opacity: 0.6; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| .selected mct-representation.s-status-pending .t-object-label .t-item-icon:before { | ||||
|     border-color: rgba($colorItemTreeSelectedFg, 0.25); | ||||
|     border-top-color: rgba($colorItemTreeSelectedFg, 1.0); | ||||
| } | ||||
|   | ||||
| @@ -270,6 +270,7 @@ | ||||
|     .splitter-treeview, | ||||
|     .holder-treeview-elements { | ||||
|         opacity: 0; | ||||
|         pointer-events: none; | ||||
|     } | ||||
| } | ||||
|  | ||||
| @@ -304,6 +305,7 @@ | ||||
|         .l-inspect, | ||||
|         .splitter-inspect { | ||||
|             opacity: 0; | ||||
|             pointer-events: none; | ||||
|         } | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -1,7 +1,29 @@ | ||||
| <!-- | ||||
|  Open MCT Web, Copyright (c) 2014-2015, United States Government | ||||
|  as represented by the Administrator of the National Aeronautics and Space | ||||
|  Administration. All rights reserved. | ||||
|  | ||||
|  Open MCT Web 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 Web 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. | ||||
| --> | ||||
| <span class="s-btn" | ||||
|       ng-controller="DateTimeFieldController"> | ||||
|     <input type="text" | ||||
|            ng-model="textValue" | ||||
|            ng-blur="restoreTextValue(); ngBlur()" | ||||
|            ng-class="{ error: textInvalid }"> | ||||
|     </input> | ||||
|     <a class="ui-symbol icon icon-calendar" | ||||
| @@ -11,8 +33,8 @@ | ||||
|     <mct-popup ng-if="picker.active"> | ||||
|         <div mct-click-elsewhere="picker.active = false"> | ||||
|             <mct-control key="'datetime-picker'" | ||||
|                          ng-model="ngModel" | ||||
|                          field="field" | ||||
|                          ng-model="pickerModel" | ||||
|                          field="'value'" | ||||
|                          options="{ hours: true }"> | ||||
|             </mct-control> | ||||
|         </div> | ||||
|   | ||||
| @@ -20,12 +20,14 @@ | ||||
|  at runtime from the About dialog for additional information. | ||||
| --> | ||||
| <div ng-controller="TimeRangeController"> | ||||
|     <div class="l-time-range-inputs-holder"> | ||||
|     <form class="l-time-range-inputs-holder" | ||||
|           ng-submit="updateBoundsFromForm()"> | ||||
|         <span class="l-time-range-inputs-elem ui-symbol type-icon">C</span> | ||||
|         <span class="l-time-range-input"> | ||||
|             <mct-control key="'datetime-field'" | ||||
|                          structure="{ format: parameters.format }" | ||||
|                          ng-model="ngModel.outer" | ||||
|                          structure="{ format: parameters.format, validate: validateStart }" | ||||
|                          ng-model="formModel" | ||||
|                          ng-blur="updateBoundsFromForm()" | ||||
|                          field="'start'" | ||||
|                          class="time-range-start"> | ||||
|             </mct-control> | ||||
| @@ -35,13 +37,16 @@ | ||||
|  | ||||
|         <span class="l-time-range-input" ng-controller="ToggleController as t2"> | ||||
|             <mct-control key="'datetime-field'" | ||||
|                          structure="{ format: parameters.format }" | ||||
|                          ng-model="ngModel.outer" | ||||
|                          structure="{ format: parameters.format, validate: validateEnd }" | ||||
|                          ng-model="formModel" | ||||
|                          ng-blur="updateBoundsFromForm()" | ||||
|                          field="'end'" | ||||
|                          class="time-range-end"> | ||||
|             </mct-control>  | ||||
|         </span> | ||||
|     </div> | ||||
|  | ||||
|         <input type="submit" class="hidden"> | ||||
|     </form> | ||||
|  | ||||
|     <div class="l-time-range-slider-holder"> | ||||
|         <div class="l-time-range-slider"> | ||||
|   | ||||
| @@ -20,7 +20,6 @@ | ||||
|  at runtime from the About dialog for additional information. | ||||
| --> | ||||
| <!-- DO NOT ADD SPACES BETWEEN THE SPANS - IT ADDS WHITE SPACE!! --> | ||||
| <!--<div ng-init="reps = [1,2,3]"></div>--> | ||||
| <div class='status block' | ||||
| 	 title="{{ngModel.getDescription()}}" | ||||
| 	 ng-click='ngModel.configure()' | ||||
|   | ||||
| @@ -19,7 +19,9 @@ | ||||
|  this source code distribution or the Licensing information page available | ||||
|  at runtime from the About dialog for additional information. | ||||
| --> | ||||
| <span class="t-object-label"> | ||||
| <span class="t-item-icon" ng-class="{ 'l-icon-link':location.isLink() }">{{type.getGlyph()}}</span> | ||||
| <span class='t-title-label'>{{model.name}}</span> | ||||
| </span> | ||||
| <div class="t-object-label l-flex-row flex-elem grows"> | ||||
|     <div class="t-item-icon flex-elem" ng-class="{ 'l-icon-link':location.isLink() }"> | ||||
|         <div class="t-item-icon-glyph">{{type.getGlyph()}}</div> | ||||
|     </div> | ||||
|     <div class='t-title-label flex-elem grows'>{{model.name}}</div> | ||||
| </div> | ||||
|   | ||||
| @@ -41,7 +41,7 @@ | ||||
|                                     mct-object="parent" | ||||
|                                     ng-model="ngModel" | ||||
|                                     ng-click="ngModel.selectedObject = parent" | ||||
|                                     class="location-item"> | ||||
|                                     class="location-item rep-object-label"> | ||||
|                 </mct-representation> | ||||
|             </span> | ||||
|         </li> | ||||
| @@ -54,7 +54,7 @@ | ||||
|                                     mct-object="parent" | ||||
|                                     ng-model="ngModel" | ||||
|                                     ng-click="ngModel.selectedObject = parent" | ||||
|                                     class="location-item"> | ||||
|                                     class="location-item rep-object-label"> | ||||
|                 </mct-representation> | ||||
|             </span> | ||||
|         </li> | ||||
|   | ||||
| @@ -26,41 +26,18 @@ | ||||
|             ng-class="{selected: treeNode.isSelected()}" | ||||
|             > | ||||
|             <span | ||||
|                 mct-device="desktop" | ||||
|                 class='ui-symbol view-control' | ||||
|                 class='ui-symbol view-control flex-elem' | ||||
|                 ng-class="{ 'has-children': model.composition !== undefined, expanded: toggle.isActive() }" | ||||
|                 ng-click="toggle.toggle(); treeNode.trackExpansion()" | ||||
|                 ng-if="model.composition !== undefined" | ||||
|                 > | ||||
|                 {{toggle.isActive() ? "v" : ">"}} | ||||
|             </span> | ||||
|  | ||||
|             <mct-representation | ||||
|                 mct-device="desktop" | ||||
|                 class="mobile-hide" | ||||
|                 class="rep-object-label" | ||||
|                 key="'label'" | ||||
|                 mct-object="domainObject" | ||||
|                 ng-click="treeNode.select()" | ||||
|                 > | ||||
|             </mct-representation> | ||||
|             <mct-representation | ||||
|                 mct-device="mobile" | ||||
|                 class="desktop-hide" | ||||
|                 key="'label'" | ||||
|                 mct-object="domainObject" | ||||
|                 ng-click="(model.composition === undefined) && treeNode.select(); | ||||
|                           toggle.toggle(); | ||||
|                           treeNode.trackExpansion();" | ||||
|                 > | ||||
|             </mct-representation> | ||||
|  | ||||
|             <span | ||||
|                 mct-device="mobile" | ||||
|                 class='ui-symbol view-control' | ||||
|                 ng-model="ngModel" | ||||
|                 ng-click="treeNode.select()" | ||||
|                 > | ||||
|                 } | ||||
|             </span> | ||||
|         </span> | ||||
|         <span | ||||
|             class="tree-item-subtree" | ||||
|   | ||||
							
								
								
									
										64
									
								
								platform/commonUI/general/src/UnsupportedBrowserWarning.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										64
									
								
								platform/commonUI/general/src/UnsupportedBrowserWarning.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,64 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT Web, Copyright (c) 2014-2015, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
|  * Administration. All rights reserved. | ||||
|  * | ||||
|  * Open MCT Web 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 Web 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. | ||||
|  *****************************************************************************/ | ||||
| /*global define*/ | ||||
|  | ||||
| /** | ||||
|  * This bundle provides various general-purpose UI elements, including | ||||
|  * platform styling. | ||||
|  * @namespace platform/commonUI/general | ||||
|  */ | ||||
| define( | ||||
|     [], | ||||
|     function () { | ||||
|         "use strict"; | ||||
|  | ||||
|         var WARNING_TITLE = "Unsupported browser", | ||||
|             WARNING_DESCRIPTION = [ | ||||
|                 "This software has been developed and tested", | ||||
|                 "using the latest Google Chrome,", | ||||
|                 "and may be unstable in other browsers." | ||||
|             ].join(" "), | ||||
|             MOBILE_BROWSER = "Safari", | ||||
|             DESKTOP_BROWSER = "Chrome"; | ||||
|  | ||||
|         /** | ||||
|          * Shows a warning if a user's browser is unsupported. | ||||
|          * @memberof platform/commonUI/general | ||||
|          * @constructor | ||||
|          * @param {NotificationService} notificationService the notification | ||||
|          *        service | ||||
|          */ | ||||
|         function UnsupportedBrowserWarning(notificationService, agentService) { | ||||
|             var testToBrowser = agentService.isMobile() ? | ||||
|                     MOBILE_BROWSER : DESKTOP_BROWSER; | ||||
|  | ||||
|             if (!agentService.isBrowser(testToBrowser)) { | ||||
|                 notificationService.alert({ | ||||
|                     title: WARNING_TITLE, | ||||
|                     actionText: WARNING_DESCRIPTION | ||||
|                 }); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         return UnsupportedBrowserWarning; | ||||
|     } | ||||
| ); | ||||
| @@ -53,7 +53,9 @@ define( | ||||
|                         formatter.parse($scope.textValue) !== value) { | ||||
|                     $scope.textValue = formatter.format(value); | ||||
|                     $scope.textInvalid = false; | ||||
|                     $scope.lastValidValue = $scope.textValue; | ||||
|                 } | ||||
|                 $scope.pickerModel = { value: value }; | ||||
|             } | ||||
|  | ||||
|             function updateFromView(textValue) { | ||||
| @@ -61,6 +63,17 @@ define( | ||||
|                 if (!$scope.textInvalid) { | ||||
|                     $scope.ngModel[$scope.field] = | ||||
|                         formatter.parse(textValue); | ||||
|                     $scope.lastValidValue = $scope.textValue; | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             function updateFromPicker(value) { | ||||
|                 if (value !== $scope.ngModel[$scope.field]) { | ||||
|                     $scope.ngModel[$scope.field] = value; | ||||
|                     updateFromModel(value); | ||||
|                     if ($scope.ngBlur) { | ||||
|                         $scope.ngBlur(); | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|  | ||||
| @@ -69,10 +82,18 @@ define( | ||||
|                 updateFromModel($scope.ngModel[$scope.field]); | ||||
|             } | ||||
|  | ||||
|             function restoreTextValue() { | ||||
|                 $scope.textValue = $scope.lastValidValue; | ||||
|                 updateFromView($scope.textValue); | ||||
|             } | ||||
|  | ||||
|             $scope.restoreTextValue = restoreTextValue; | ||||
|  | ||||
|             $scope.picker = { active: false }; | ||||
|  | ||||
|             $scope.$watch('structure.format', setFormat); | ||||
|             $scope.$watch('ngModel[field]', updateFromModel); | ||||
|             $scope.$watch('pickerModel.value', updateFromPicker); | ||||
|             $scope.$watch('textValue', updateFromView); | ||||
|  | ||||
|         } | ||||
|   | ||||
| @@ -175,6 +175,13 @@ define( | ||||
|                 updateViewFromModel($scope.ngModel); | ||||
|             } | ||||
|  | ||||
|             function updateFormModel() { | ||||
|                 $scope.formModel = { | ||||
|                     start: (($scope.ngModel || {}).outer || {}).start, | ||||
|                     end: (($scope.ngModel || {}).outer || {}).end | ||||
|                 }; | ||||
|             } | ||||
|  | ||||
|             function updateOuterStart(t) { | ||||
|                 var ngModel = $scope.ngModel; | ||||
|  | ||||
| @@ -192,6 +199,7 @@ define( | ||||
|                     ngModel.inner.end | ||||
|                 ); | ||||
|  | ||||
|                 updateFormModel(); | ||||
|                 updateViewForInnerSpanFromModel(ngModel); | ||||
|                 updateTicks(); | ||||
|             } | ||||
| @@ -213,6 +221,7 @@ define( | ||||
|                     ngModel.inner.start | ||||
|                 ); | ||||
|  | ||||
|                 updateFormModel(); | ||||
|                 updateViewForInnerSpanFromModel(ngModel); | ||||
|                 updateTicks(); | ||||
|             } | ||||
| @@ -223,6 +232,22 @@ define( | ||||
|                 updateTicks(); | ||||
|             } | ||||
|  | ||||
|             function updateBoundsFromForm() { | ||||
|                 $scope.ngModel = $scope.ngModel || {}; | ||||
|                 $scope.ngModel.outer = { | ||||
|                     start: $scope.formModel.start, | ||||
|                     end: $scope.formModel.end | ||||
|                 }; | ||||
|             } | ||||
|  | ||||
|             function validateStart(startValue) { | ||||
|                 return startValue <= $scope.ngModel.outer.end - outerMinimumSpan; | ||||
|             } | ||||
|  | ||||
|             function validateEnd(endValue) { | ||||
|                 return endValue >= $scope.ngModel.outer.start + outerMinimumSpan; | ||||
|             } | ||||
|  | ||||
|             $scope.startLeftDrag = startLeftDrag; | ||||
|             $scope.startRightDrag = startRightDrag; | ||||
|             $scope.startMiddleDrag = startMiddleDrag; | ||||
| @@ -230,10 +255,16 @@ define( | ||||
|             $scope.rightDrag = rightDrag; | ||||
|             $scope.middleDrag = middleDrag; | ||||
|  | ||||
|             $scope.updateBoundsFromForm = updateBoundsFromForm; | ||||
|  | ||||
|             $scope.validateStart = validateStart; | ||||
|             $scope.validateEnd = validateEnd; | ||||
|  | ||||
|             $scope.ticks = []; | ||||
|  | ||||
|             // Initialize scope to defaults | ||||
|             updateViewFromModel($scope.ngModel); | ||||
|             updateFormModel(); | ||||
|  | ||||
|             $scope.$watchCollection("ngModel", updateViewFromModel); | ||||
|             $scope.$watch("spanWidth", updateSpanWidth); | ||||
|   | ||||
| @@ -204,7 +204,7 @@ define( | ||||
|                 // And poll for position changes enforced by styles | ||||
|                 activeInterval = $interval(function () { | ||||
|                     getSetPosition(getSetPosition()); | ||||
|                 }, POLLING_INTERVAL, false); | ||||
|                 }, POLLING_INTERVAL, 0, false); | ||||
|  | ||||
|                 // ...and stop polling when we're destroyed. | ||||
|                 $scope.$on('$destroy', function () { | ||||
|   | ||||
| @@ -0,0 +1,98 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT Web, Copyright (c) 2014-2015, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
|  * Administration. All rights reserved. | ||||
|  * | ||||
|  * Open MCT Web 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 Web 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. | ||||
|  *****************************************************************************/ | ||||
| /*global define,Promise,describe,it,expect,beforeEach,waitsFor,jasmine*/ | ||||
|  | ||||
| define( | ||||
|     ["../src/UnsupportedBrowserWarning"], | ||||
|     function (UnsupportedBrowserWarning) { | ||||
|         "use strict"; | ||||
|  | ||||
|         var MOBILE_BROWSER = "Safari", | ||||
|             DESKTOP_BROWSER = "Chrome", | ||||
|             UNSUPPORTED_BROWSERS = [ | ||||
|                 "Firefox", | ||||
|                 "IE", | ||||
|                 "Opera", | ||||
|                 "Iceweasel" | ||||
|             ]; | ||||
|  | ||||
|         describe("The unsupported browser warning", function () { | ||||
|             var mockNotificationService, | ||||
|                 mockAgentService, | ||||
|                 testAgent; | ||||
|  | ||||
|             function instantiateWith(browser) { | ||||
|                 testAgent = "Mozilla/5.0 " + browser + "/12.34.56"; | ||||
|                 return new UnsupportedBrowserWarning( | ||||
|                     mockNotificationService, | ||||
|                     mockAgentService | ||||
|                 ); | ||||
|             } | ||||
|  | ||||
|             beforeEach(function () { | ||||
|                 testAgent = "chrome"; | ||||
|                 mockNotificationService = jasmine.createSpyObj( | ||||
|                     "notificationService", | ||||
|                     [ "alert" ] | ||||
|                 ); | ||||
|                 mockAgentService = jasmine.createSpyObj( | ||||
|                     "agentService", | ||||
|                     [ "isMobile", "isBrowser" ] | ||||
|                 ); | ||||
|                 mockAgentService.isBrowser.andCallFake(function (substr) { | ||||
|                     substr = substr.toLowerCase(); | ||||
|                     return testAgent.toLowerCase().indexOf(substr) !== -1; | ||||
|                 }); | ||||
|             }); | ||||
|  | ||||
|             [ false, true ].forEach(function (isMobile) { | ||||
|                 var deviceType = isMobile ? "mobile" : "desktop", | ||||
|                     goodBrowser = isMobile ? MOBILE_BROWSER : DESKTOP_BROWSER, | ||||
|                     badBrowsers = UNSUPPORTED_BROWSERS.concat([ | ||||
|                         isMobile ? DESKTOP_BROWSER : MOBILE_BROWSER | ||||
|                     ]); | ||||
|  | ||||
|                 describe("on " + deviceType + " devices", function () { | ||||
|                     beforeEach(function () { | ||||
|                         mockAgentService.isMobile.andReturn(isMobile); | ||||
|                     }); | ||||
|  | ||||
|                     it("is not shown for " + goodBrowser, function () { | ||||
|                         instantiateWith(goodBrowser); | ||||
|                         expect(mockNotificationService.alert) | ||||
|                             .not.toHaveBeenCalled(); | ||||
|                     }); | ||||
|  | ||||
|                     badBrowsers.forEach(function (badBrowser) { | ||||
|                         it("is shown for " + badBrowser, function () { | ||||
|                             instantiateWith(badBrowser); | ||||
|                             expect(mockNotificationService.alert) | ||||
|                                 .toHaveBeenCalled(); | ||||
|                         }); | ||||
|                     }); | ||||
|                 }); | ||||
|             }); | ||||
|  | ||||
|         }); | ||||
|     } | ||||
| ); | ||||
|  | ||||
| @@ -67,21 +67,13 @@ define( | ||||
|                 mockScope.ngModel = { testField: 12321 }; | ||||
|                 mockScope.field = "testField"; | ||||
|                 mockScope.structure = { format: "someFormat" }; | ||||
|                 mockScope.ngBlur = jasmine.createSpy('blur'); | ||||
|  | ||||
|                 controller = new DateTimeFieldController( | ||||
|                     mockScope, | ||||
|                     mockFormatService | ||||
|                 ); | ||||
|             }); | ||||
|  | ||||
|             it("updates models from user-entered text", function () { | ||||
|                 var newText = "1977-05-25 17:30:00"; | ||||
|  | ||||
|                 mockScope.textValue = newText; | ||||
|                 fireWatch("textValue", newText); | ||||
|                 expect(mockScope.ngModel.testField) | ||||
|                     .toEqual(mockFormat.parse(newText)); | ||||
|                 expect(mockScope.textInvalid).toBeFalsy(); | ||||
|                 fireWatch("ngModel[field]", mockScope.ngModel.testField); | ||||
|             }); | ||||
|  | ||||
|             it("updates text from model values", function () { | ||||
| @@ -91,16 +83,55 @@ define( | ||||
|                 expect(mockScope.textValue).toEqual("1977-05-25 17:30:00"); | ||||
|             }); | ||||
|  | ||||
|             describe("when valid text is entered", function () { | ||||
|                 var newText; | ||||
|  | ||||
|                 beforeEach(function () { | ||||
|                     newText = "1977-05-25 17:30:00"; | ||||
|                     mockScope.textValue = newText; | ||||
|                     fireWatch("textValue", newText); | ||||
|                 }); | ||||
|  | ||||
|                 it("updates models from user-entered text", function () { | ||||
|                     expect(mockScope.ngModel.testField) | ||||
|                         .toEqual(mockFormat.parse(newText)); | ||||
|                     expect(mockScope.textInvalid).toBeFalsy(); | ||||
|                 }); | ||||
|  | ||||
|                 it("does not indicate a blur event", function () { | ||||
|                     expect(mockScope.ngBlur).not.toHaveBeenCalled(); | ||||
|                 }); | ||||
|             }); | ||||
|  | ||||
|             describe("when a date is chosen via the date picker", function () { | ||||
|                 var newValue; | ||||
|  | ||||
|                 beforeEach(function () { | ||||
|                     newValue = 12345654321; | ||||
|                     mockScope.pickerModel.value = newValue; | ||||
|                     fireWatch("pickerModel.value", newValue); | ||||
|                 }); | ||||
|  | ||||
|                 it("updates models", function () { | ||||
|                     expect(mockScope.ngModel.testField).toEqual(newValue); | ||||
|                 }); | ||||
|  | ||||
|                 it("fires a blur event", function () { | ||||
|                     expect(mockScope.ngBlur).toHaveBeenCalled(); | ||||
|                 }); | ||||
|             }); | ||||
|  | ||||
|             it("exposes toggle state for date-time picker", function () { | ||||
|                 expect(mockScope.picker.active).toBe(false); | ||||
|             }); | ||||
|  | ||||
|             describe("when user input is invalid", function () { | ||||
|                 var newText, oldValue; | ||||
|                 var newText, oldText, oldValue; | ||||
|  | ||||
|                 beforeEach(function () { | ||||
|                     newText = "Not a date"; | ||||
|                     oldValue = mockScope.ngModel.testField; | ||||
|                     oldText = mockScope.textValue; | ||||
|                     mockScope.textValue = newText; | ||||
|                     fireWatch("textValue", newText); | ||||
|                 }); | ||||
| @@ -116,6 +147,11 @@ define( | ||||
|                 it("does not modify user input", function () { | ||||
|                     expect(mockScope.textValue).toEqual(newText); | ||||
|                 }); | ||||
|  | ||||
|                 it("restores valid text values on request", function () { | ||||
|                     mockScope.restoreTextValue(); | ||||
|                     expect(mockScope.textValue).toEqual(oldText); | ||||
|                 }); | ||||
|             }); | ||||
|  | ||||
|             it("does not modify valid but irregular user input", function () { | ||||
|   | ||||
| @@ -91,6 +91,39 @@ define( | ||||
|                     .toHaveBeenCalledWith("ngModel", jasmine.any(Function)); | ||||
|             }); | ||||
|  | ||||
|             describe("when changes are made via form entry", function () { | ||||
|                 beforeEach(function () { | ||||
|                     mockScope.ngModel = { | ||||
|                         outer: { start: DAY * 2, end: DAY * 3 }, | ||||
|                         inner: { start: DAY * 2.25, end: DAY * 2.75 } | ||||
|                     }; | ||||
|                     mockScope.formModel = { | ||||
|                         start: DAY * 10000, | ||||
|                         end: DAY * 11000 | ||||
|                     }; | ||||
|                     // These watches may not exist, but Angular would fire | ||||
|                     // them if they did. | ||||
|                     fireWatchCollection("formModel", mockScope.formModel); | ||||
|                     fireWatch("formModel.start", mockScope.formModel.start); | ||||
|                     fireWatch("formModel.end", mockScope.formModel.end); | ||||
|                 }); | ||||
|  | ||||
|                 it("does not immediately make changes to the model", function () { | ||||
|                     expect(mockScope.ngModel.outer.start) | ||||
|                         .not.toEqual(mockScope.formModel.start); | ||||
|                     expect(mockScope.ngModel.outer.end) | ||||
|                         .not.toEqual(mockScope.formModel.end); | ||||
|                 }); | ||||
|  | ||||
|                 it("updates model bounds on request", function () { | ||||
|                     mockScope.updateBoundsFromForm(); | ||||
|                     expect(mockScope.ngModel.outer.start) | ||||
|                         .toEqual(mockScope.formModel.start); | ||||
|                     expect(mockScope.ngModel.outer.end) | ||||
|                         .toEqual(mockScope.formModel.end); | ||||
|                 }); | ||||
|             }); | ||||
|  | ||||
|             describe("when dragged", function () { | ||||
|                 beforeEach(function () { | ||||
|                     mockScope.ngModel = { | ||||
|   | ||||
| @@ -0,0 +1,95 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT Web, Copyright (c) 2014-2015, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
|  * Administration. All rights reserved. | ||||
|  * | ||||
|  * Open MCT Web 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 Web 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. | ||||
|  *****************************************************************************/ | ||||
| /*global define,Promise,describe,it,expect,beforeEach,waitsFor,jasmine*/ | ||||
|  | ||||
| define( | ||||
|     ["../../src/directives/MCTSplitPane"], | ||||
|     function (MCTSplitPane) { | ||||
|         'use strict'; | ||||
|  | ||||
|         var JQLITE_METHODS = [ | ||||
|                 'on', | ||||
|                 'addClass', | ||||
|                 'children', | ||||
|                 'eq' | ||||
|             ]; | ||||
|  | ||||
|         describe("The mct-split-pane directive", function () { | ||||
|             var mockParse, | ||||
|                 mockLog, | ||||
|                 mockInterval, | ||||
|                 mctSplitPane; | ||||
|  | ||||
|             beforeEach(function () { | ||||
|                 mockParse = jasmine.createSpy('$parse'); | ||||
|                 mockLog = | ||||
|                     jasmine.createSpyObj('$log', ['warn', 'info', 'debug']); | ||||
|                 mockInterval = jasmine.createSpy('$interval'); | ||||
|                 mockInterval.cancel = jasmine.createSpy('mockCancel'); | ||||
|                 mctSplitPane = new MCTSplitPane( | ||||
|                     mockParse, | ||||
|                     mockLog, | ||||
|                     mockInterval | ||||
|                 ); | ||||
|             }); | ||||
|  | ||||
|             it("is only applicable as an element", function () { | ||||
|                 expect(mctSplitPane.restrict).toEqual("E"); | ||||
|             }); | ||||
|  | ||||
|             describe("when its controller is applied", function () { | ||||
|                 var mockScope, | ||||
|                     mockElement, | ||||
|                     testAttrs, | ||||
|                     mockChildren, | ||||
|                     controller; | ||||
|  | ||||
|                 beforeEach(function () { | ||||
|                     mockScope = | ||||
|                         jasmine.createSpyObj('$scope', ['$apply', '$watch', '$on']); | ||||
|                     mockElement = | ||||
|                         jasmine.createSpyObj('element', JQLITE_METHODS); | ||||
|                     testAttrs = {}; | ||||
|                     mockChildren = | ||||
|                         jasmine.createSpyObj('children', JQLITE_METHODS); | ||||
|  | ||||
|                     mockElement.children.andReturn(mockChildren); | ||||
|                     mockChildren.eq.andReturn(mockChildren); | ||||
|                     mockChildren[0] = {}; | ||||
|  | ||||
|                     controller = mctSplitPane.controller[3]( | ||||
|                         mockScope, | ||||
|                         mockElement, | ||||
|                         testAttrs | ||||
|                     ); | ||||
|                 }); | ||||
|  | ||||
|                 it("sets an interval which does not trigger digests", function () { | ||||
|                     expect(mockInterval.mostRecentCall.args[3]).toBe(false); | ||||
|                 }); | ||||
|  | ||||
|             }); | ||||
|  | ||||
|         }); | ||||
|  | ||||
|     } | ||||
| ); | ||||
| @@ -19,8 +19,10 @@ | ||||
|     "directives/MCTPopup", | ||||
|     "directives/MCTResize", | ||||
|     "directives/MCTScroll", | ||||
|     "directives/MCTSplitPane", | ||||
|     "services/Popup", | ||||
|     "services/PopupService", | ||||
|     "services/UrlService", | ||||
|     "StyleSheetLoader" | ||||
|     "StyleSheetLoader", | ||||
|     "UnsupportedBrowserWarning" | ||||
| ] | ||||
|   | ||||
| @@ -13,6 +13,12 @@ | ||||
|                 "implementation": "AgentService.js", | ||||
|                 "depends": [ "$window" ] | ||||
|             } | ||||
|         ], | ||||
|         "runs": [ | ||||
|             { | ||||
|                 "implementation": "DeviceClassifier.js", | ||||
|                 "depends": [ "agentService", "$document" ] | ||||
|             } | ||||
|         ] | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -43,8 +43,10 @@ define( | ||||
|             var userAgent = $window.navigator.userAgent, | ||||
|                 matches = userAgent.match(/iPad|iPhone|Android/i) || []; | ||||
|  | ||||
|             this.userAgent = userAgent; | ||||
|             this.mobileName = matches[0]; | ||||
|             this.$window = $window; | ||||
|             this.touchEnabled = ($window.ontouchstart !== undefined); | ||||
|         } | ||||
|  | ||||
|         /** | ||||
| @@ -91,6 +93,26 @@ define( | ||||
|             return !this.isPortrait(); | ||||
|         }; | ||||
|  | ||||
|         /** | ||||
|          * Check if the user's device supports a touch interface. | ||||
|          * @returns {boolean} true if touch is supported | ||||
|          */ | ||||
|         AgentService.prototype.isTouch = function () { | ||||
|             return this.touchEnabled; | ||||
|         }; | ||||
|  | ||||
|         /** | ||||
|          * Check if the user agent matches a certain named device, | ||||
|          * as indicated by checking for a case-insensitive substring | ||||
|          * match. | ||||
|          * @param {string} name the name to check for | ||||
|          * @returns {boolean} true if the user agent includes that name | ||||
|          */ | ||||
|         AgentService.prototype.isBrowser = function (name) { | ||||
|             name = name.toLowerCase(); | ||||
|             return this.userAgent.toLowerCase().indexOf(name) !== -1; | ||||
|         }; | ||||
|  | ||||
|         return AgentService; | ||||
|     } | ||||
| ); | ||||
|   | ||||
							
								
								
									
										59
									
								
								platform/commonUI/mobile/src/DeviceClassifier.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								platform/commonUI/mobile/src/DeviceClassifier.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,59 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT Web, Copyright (c) 2014-2015, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
|  * Administration. All rights reserved. | ||||
|  * | ||||
|  * Open MCT Web 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 Web 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. | ||||
|  *****************************************************************************/ | ||||
| /*global define,Promise*/ | ||||
|  | ||||
| define( | ||||
|     ['./DeviceMatchers'], | ||||
|     function (DeviceMatchers) { | ||||
|         'use strict'; | ||||
|  | ||||
|         /** | ||||
|          * Runs at application startup and adds a subset of the following | ||||
|          * CSS classes to the body of the document, depending on device | ||||
|          * attributes: | ||||
|          * | ||||
|          * * `mobile`: Phones or tablets. | ||||
|          * * `phone`: Phones specifically. | ||||
|          * * `tablet`: Tablets specifically. | ||||
|          * * `desktop`: Non-mobile devices. | ||||
|          * * `portrait`: Devices in a portrait-style orientation. | ||||
|          * * `landscape`: Devices in a landscape-style orientation. | ||||
|          * * `touch`: Device supports touch events. | ||||
|          * | ||||
|          * @param {platform/commonUI/mobile.AgentService} agentService | ||||
|          *        the service used to examine the user agent | ||||
|          * @param $document Angular's jqLite-wrapped document element | ||||
|          * @constructor | ||||
|          */ | ||||
|         function MobileClassifier(agentService, $document) { | ||||
|             var body = $document.find('body'); | ||||
|             Object.keys(DeviceMatchers).forEach(function (key) { | ||||
|                 if (DeviceMatchers[key](agentService)) { | ||||
|                     body.addClass(key); | ||||
|                 } | ||||
|             }); | ||||
|         } | ||||
|  | ||||
|         return MobileClassifier; | ||||
|  | ||||
|     } | ||||
| ); | ||||
							
								
								
									
										60
									
								
								platform/commonUI/mobile/src/DeviceMatchers.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										60
									
								
								platform/commonUI/mobile/src/DeviceMatchers.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,60 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT Web, Copyright (c) 2014-2015, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
|  * Administration. All rights reserved. | ||||
|  * | ||||
|  * Open MCT Web 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 Web 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. | ||||
|  *****************************************************************************/ | ||||
| /*global define*/ | ||||
| define(function () { | ||||
|     "use strict"; | ||||
|  | ||||
|     /** | ||||
|      * An object containing key-value pairs, where keys are symbolic of | ||||
|      * device attributes, and values are functions that take the | ||||
|      * `agentService` as inputs and return boolean values indicating | ||||
|      * whether or not the current device has these attributes. | ||||
|      * | ||||
|      * For internal use by the mobile support bundle. | ||||
|      * | ||||
|      * @memberof platform/commonUI/mobile | ||||
|      * @private | ||||
|      */ | ||||
|     return { | ||||
|         mobile: function (agentService) { | ||||
|             return agentService.isMobile(); | ||||
|         }, | ||||
|         phone: function (agentService) { | ||||
|             return agentService.isPhone(); | ||||
|         }, | ||||
|         tablet: function (agentService) { | ||||
|             return agentService.isTablet(); | ||||
|         }, | ||||
|         desktop: function (agentService) { | ||||
|             return !agentService.isMobile(); | ||||
|         }, | ||||
|         portrait: function (agentService) { | ||||
|             return agentService.isPortrait(); | ||||
|         }, | ||||
|         landscape: function (agentService) { | ||||
|             return agentService.isLandscape(); | ||||
|         }, | ||||
|         touch: function (agentService) { | ||||
|             return agentService.isTouch(); | ||||
|         } | ||||
|     }; | ||||
| }); | ||||
| @@ -22,31 +22,10 @@ | ||||
| /*global define,Promise*/ | ||||
|  | ||||
| define( | ||||
|     function () { | ||||
|     ['./DeviceMatchers'], | ||||
|     function (DeviceMatchers) { | ||||
|         'use strict'; | ||||
|  | ||||
|         var DEVICE_MATCHERS = { | ||||
|             mobile: function (agentService) { | ||||
|                 return agentService.isMobile(); | ||||
|             }, | ||||
|             phone: function (agentService) { | ||||
|                 return agentService.isPhone(); | ||||
|             }, | ||||
|             tablet: function (agentService) { | ||||
|                 return agentService.isTablet(); | ||||
|             }, | ||||
|             desktop: function (agentService) { | ||||
|                 return !agentService.isMobile(); | ||||
|             }, | ||||
|             portrait: function (agentService) { | ||||
|                 return agentService.isPortrait(); | ||||
|             }, | ||||
|             landscape: function (agentService) { | ||||
|                 return agentService.isLandscape(); | ||||
|             } | ||||
|         }; | ||||
|  | ||||
|  | ||||
|         /** | ||||
|          * The `mct-device` directive, when applied as an attribute, | ||||
|          * only includes the element when the device being used matches | ||||
| @@ -68,6 +47,7 @@ define( | ||||
|          * * `desktop`: Non-mobile devices. | ||||
|          * * `portrait`: Devices in a portrait-style orientation. | ||||
|          * * `landscape`: Devices in a landscape-style orientation. | ||||
|          * * `touch`: Device supports touch events. | ||||
|          * | ||||
|          * @param {AgentService} agentService used to detect device type | ||||
|          *        based on information about the user agent | ||||
| @@ -77,7 +57,7 @@ define( | ||||
|             function deviceMatches(tokens) { | ||||
|                 tokens = tokens || ""; | ||||
|                 return tokens.split(" ").every(function (token) { | ||||
|                     var fn = DEVICE_MATCHERS[token]; | ||||
|                     var fn = DeviceMatchers[token]; | ||||
|                     return fn && fn(agentService); | ||||
|                 }); | ||||
|             } | ||||
|   | ||||
| @@ -81,6 +81,22 @@ define( | ||||
|                 expect(agentService.isPortrait()).toBeTruthy(); | ||||
|                 expect(agentService.isLandscape()).toBeFalsy(); | ||||
|             }); | ||||
|  | ||||
|             it("detects touch support", function () { | ||||
|                 testWindow.ontouchstart = null; | ||||
|                 expect(new AgentService(testWindow).isTouch()) | ||||
|                     .toBe(true); | ||||
|                 delete testWindow.ontouchstart; | ||||
|                 expect(new AgentService(testWindow).isTouch()) | ||||
|                     .toBe(false); | ||||
|             }); | ||||
|  | ||||
|             it("allows for checking browser type", function () { | ||||
|                 testWindow.navigator.userAgent = "Chromezilla Safarifox"; | ||||
|                 agentService = new AgentService(testWindow); | ||||
|                 expect(agentService.isBrowser("Chrome")).toBe(true); | ||||
|                 expect(agentService.isBrowser("Firefox")).toBe(false); | ||||
|             }); | ||||
|         }); | ||||
|     } | ||||
| ); | ||||
|   | ||||
							
								
								
									
										112
									
								
								platform/commonUI/mobile/test/DeviceClassifierSpec.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										112
									
								
								platform/commonUI/mobile/test/DeviceClassifierSpec.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,112 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT Web, Copyright (c) 2014-2015, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
|  * Administration. All rights reserved. | ||||
|  * | ||||
|  * Open MCT Web 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 Web 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. | ||||
|  *****************************************************************************/ | ||||
| /*global define,Promise,describe,it,expect,beforeEach,waitsFor,jasmine*/ | ||||
|  | ||||
|  | ||||
| define( | ||||
|     ["../src/DeviceClassifier", "../src/DeviceMatchers"], | ||||
|     function (DeviceClassifier, DeviceMatchers) { | ||||
|         "use strict"; | ||||
|  | ||||
|         var AGENT_SERVICE_METHODS = [ | ||||
|                 'isMobile', | ||||
|                 'isPhone', | ||||
|                 'isTablet', | ||||
|                 'isPortrait', | ||||
|                 'isLandscape', | ||||
|                 'isTouch' | ||||
|             ], | ||||
|             TEST_PERMUTATIONS = [ | ||||
|                 [ 'isMobile', 'isPhone', 'isTouch', 'isPortrait' ], | ||||
|                 [ 'isMobile', 'isPhone', 'isTouch', 'isLandscape' ], | ||||
|                 [ 'isMobile', 'isTablet', 'isTouch', 'isPortrait' ], | ||||
|                 [ 'isMobile', 'isTablet', 'isTouch', 'isLandscape' ], | ||||
|                 [ 'isTouch' ], | ||||
|                 [] | ||||
|             ]; | ||||
|  | ||||
|         describe("DeviceClassifier", function () { | ||||
|             var mockAgentService, | ||||
|                 mockDocument, | ||||
|                 mockBody; | ||||
|  | ||||
|             beforeEach(function () { | ||||
|                 mockAgentService = jasmine.createSpyObj( | ||||
|                     'agentService', | ||||
|                     AGENT_SERVICE_METHODS | ||||
|                 ); | ||||
|                 mockDocument = jasmine.createSpyObj( | ||||
|                     '$document', | ||||
|                     [ 'find' ] | ||||
|                 ); | ||||
|                 mockBody = jasmine.createSpyObj( | ||||
|                     'body', | ||||
|                     [ 'addClass' ] | ||||
|                 ); | ||||
|                 mockDocument.find.andCallFake(function (sel) { | ||||
|                     return sel === 'body' && mockBody; | ||||
|                 }); | ||||
|                 AGENT_SERVICE_METHODS.forEach(function (m) { | ||||
|                     mockAgentService[m].andReturn(false); | ||||
|                 }); | ||||
|             }); | ||||
|  | ||||
|             TEST_PERMUTATIONS.forEach(function (trueMethods) { | ||||
|                 var summary = trueMethods.length === 0 ? | ||||
|                         "device has no detected characteristics" : | ||||
|                         "device " + (trueMethods.join(", ")); | ||||
|  | ||||
|                 describe("when " + summary, function () { | ||||
|                     var classifier; | ||||
|  | ||||
|                     beforeEach(function () { | ||||
|                         trueMethods.forEach(function (m) { | ||||
|                             mockAgentService[m].andReturn(true); | ||||
|                         }); | ||||
|                         classifier = new DeviceClassifier( | ||||
|                             mockAgentService, | ||||
|                             mockDocument | ||||
|                         ); | ||||
|                     }); | ||||
|  | ||||
|                     it("adds classes for matching, detected characteristics", function () { | ||||
|                         Object.keys(DeviceMatchers).filter(function (m) { | ||||
|                             return DeviceMatchers[m](mockAgentService); | ||||
|                         }).forEach(function (key) { | ||||
|                             expect(mockBody.addClass) | ||||
|                                 .toHaveBeenCalledWith(key); | ||||
|                         }); | ||||
|                     }); | ||||
|  | ||||
|                     it("does not add classes for non-matching characteristics", function () { | ||||
|                         Object.keys(DeviceMatchers).filter(function (m) { | ||||
|                             return !DeviceMatchers[m](mockAgentService); | ||||
|                         }).forEach(function (key) { | ||||
|                             expect(mockBody.addClass) | ||||
|                                 .not.toHaveBeenCalledWith(key); | ||||
|                         }); | ||||
|                     }); | ||||
|                 }); | ||||
|             }); | ||||
|         }); | ||||
|     } | ||||
| ); | ||||
							
								
								
									
										81
									
								
								platform/commonUI/mobile/test/DeviceMatchersSpec.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										81
									
								
								platform/commonUI/mobile/test/DeviceMatchersSpec.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,81 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT Web, Copyright (c) 2014-2015, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
|  * Administration. All rights reserved. | ||||
|  * | ||||
|  * Open MCT Web 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 Web 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. | ||||
|  *****************************************************************************/ | ||||
| /*global define,Promise,describe,it,expect,beforeEach,waitsFor,jasmine*/ | ||||
|  | ||||
|  | ||||
| define( | ||||
|     ["../src/DeviceMatchers"], | ||||
|     function (DeviceMatchers) { | ||||
|         'use strict'; | ||||
|  | ||||
|         describe("DeviceMatchers", function () { | ||||
|             var mockAgentService; | ||||
|  | ||||
|             beforeEach(function () { | ||||
|                 mockAgentService = jasmine.createSpyObj( | ||||
|                     'agentService', | ||||
|                     [ | ||||
|                         'isMobile', | ||||
|                         'isPhone', | ||||
|                         'isTablet', | ||||
|                         'isPortrait', | ||||
|                         'isLandscape', | ||||
|                         'isTouch' | ||||
|                     ] | ||||
|                 ); | ||||
|             }); | ||||
|  | ||||
|             it("detects when a device is a desktop device", function () { | ||||
|                 mockAgentService.isMobile.andReturn(false); | ||||
|                 expect(DeviceMatchers.desktop(mockAgentService)) | ||||
|                     .toBe(true); | ||||
|                 mockAgentService.isMobile.andReturn(true); | ||||
|                 expect(DeviceMatchers.desktop(mockAgentService)) | ||||
|                     .toBe(false); | ||||
|             }); | ||||
|  | ||||
|             function method(deviceType) { | ||||
|                 return "is" + deviceType[0].toUpperCase() + deviceType.slice(1); | ||||
|             } | ||||
|  | ||||
|             [ | ||||
|                 "mobile", | ||||
|                 "phone", | ||||
|                 "tablet", | ||||
|                 "landscape", | ||||
|                 "portrait", | ||||
|                 "landscape", | ||||
|                 "touch" | ||||
|             ].forEach(function (deviceType) { | ||||
|                 it("detects when a device is a " + deviceType + " device", function () { | ||||
|                     mockAgentService[method(deviceType)].andReturn(true); | ||||
|                     expect(DeviceMatchers[deviceType](mockAgentService)) | ||||
|                         .toBe(true); | ||||
|                     mockAgentService[method(deviceType)].andReturn(false); | ||||
|                     expect(DeviceMatchers[deviceType](mockAgentService)) | ||||
|                         .toBe(false); | ||||
|                 }); | ||||
|             }); | ||||
|  | ||||
|         }); | ||||
|     } | ||||
| ); | ||||
| @@ -1,4 +1,6 @@ | ||||
| [ | ||||
|     "AgentService", | ||||
|     "DeviceClassifier", | ||||
|     "DeviceMatchers", | ||||
|     "MCTDevice" | ||||
| ] | ||||
|   | ||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -188,7 +188,8 @@ | ||||
|             { | ||||
|                 "key": "persistence", | ||||
|                 "implementation": "capabilities/PersistenceCapability.js", | ||||
|                 "depends": [ "persistenceService", "identifierService" ] | ||||
|                 "depends": [ "persistenceService", "identifierService", | ||||
|                     "notificationService", "$q" ] | ||||
|             }, | ||||
|             { | ||||
|                 "key": "metadata", | ||||
|   | ||||
| @@ -20,6 +20,7 @@ | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
| /*global define*/ | ||||
| /*jslint es5: true */ | ||||
|  | ||||
|  | ||||
| define( | ||||
| @@ -47,6 +48,8 @@ define( | ||||
|         function PersistenceCapability( | ||||
|             persistenceService, | ||||
|             identifierService, | ||||
|             notificationService, | ||||
|             $q, | ||||
|             domainObject | ||||
|         ) { | ||||
|             // Cache modified timestamp | ||||
| @@ -55,6 +58,8 @@ define( | ||||
|             this.domainObject = domainObject; | ||||
|             this.identifierService = identifierService; | ||||
|             this.persistenceService = persistenceService; | ||||
|             this.notificationService = notificationService; | ||||
|             this.$q = $q; | ||||
|         } | ||||
|  | ||||
|         // Utility function for creating promise-like objects which | ||||
| @@ -72,6 +77,46 @@ define( | ||||
|             return parts.length > 1 ? parts.slice(1).join(":") : id; | ||||
|         } | ||||
|  | ||||
|         /** | ||||
|          * Checks if the value returned is falsey, and if so returns a | ||||
|          * rejected promise | ||||
|          */ | ||||
|         function rejectIfFalsey(value, $q){ | ||||
|             if (!value){ | ||||
|                 return $q.reject("Error persisting object"); | ||||
|             } else { | ||||
|                 return value; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         function formatError(error){ | ||||
|             if (error && error.message) { | ||||
|                 return error.message; | ||||
|             } else if (error && typeof error === "string"){ | ||||
|                 return error; | ||||
|             } else { | ||||
|                 return "unknown error"; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         /** | ||||
|          * Display a notification message if an error has occurred during | ||||
|          * persistence. | ||||
|          */ | ||||
|         function notifyOnError(error, domainObject, notificationService, $q){ | ||||
|             var errorMessage = "Unable to persist " + domainObject.getModel().name; | ||||
|             if (error) { | ||||
|                 errorMessage += ": " + formatError(error); | ||||
|             } | ||||
|  | ||||
|             notificationService.error({ | ||||
|                 title: "Error persisting " + domainObject.getModel().name, | ||||
|                 hint: errorMessage || "Unknown error" | ||||
|             }); | ||||
|  | ||||
|             return $q.reject(error); | ||||
|         } | ||||
|  | ||||
|         /** | ||||
|          * Persist any changes which have been made to this | ||||
|          * domain object's model. | ||||
| @@ -80,7 +125,8 @@ define( | ||||
|          *          if not. | ||||
|          */ | ||||
|         PersistenceCapability.prototype.persist = function () { | ||||
|             var domainObject = this.domainObject, | ||||
|             var self = this, | ||||
|                 domainObject = this.domainObject, | ||||
|                 model = domainObject.getModel(), | ||||
|                 modified = model.modified, | ||||
|                 persistenceService = this.persistenceService, | ||||
| @@ -98,7 +144,11 @@ define( | ||||
|                 this.getSpace(), | ||||
|                 getKey(domainObject.getId()), | ||||
|                 domainObject.getModel() | ||||
|             ]); | ||||
|             ]).then(function(result){ | ||||
|                 return rejectIfFalsey(result, self.$q); | ||||
|             }).catch(function(error){ | ||||
|                 return notifyOnError(error, domainObject, self.notificationService, self.$q); | ||||
|             }); | ||||
|         }; | ||||
|  | ||||
|         /** | ||||
|   | ||||
| @@ -20,6 +20,7 @@ | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
| /*global define,Promise,describe,it,expect,beforeEach,waitsFor,jasmine*/ | ||||
| /*jslint es5: true */ | ||||
|  | ||||
| /** | ||||
|  * PersistenceCapabilitySpec. Created by vwoeltje on 11/6/14. | ||||
| @@ -34,24 +35,36 @@ define( | ||||
|                 mockIdentifierService, | ||||
|                 mockDomainObject, | ||||
|                 mockIdentifier, | ||||
|                 mockNofificationService, | ||||
|                 mockQ, | ||||
|                 id = "object id", | ||||
|                 model = { someKey: "some value"}, | ||||
|                 model, | ||||
|                 SPACE = "some space", | ||||
|                 persistence; | ||||
|                 persistence, | ||||
|                 happyPromise; | ||||
|  | ||||
|             function asPromise(value) { | ||||
|             function asPromise(value, doCatch) { | ||||
|                 return (value || {}).then ? value : { | ||||
|                     then: function (callback) { | ||||
|                         return asPromise(callback(value)); | ||||
|                     }, | ||||
|                     catch: function(callback) { | ||||
|                         //Define a default 'happy' catch, that skips over the | ||||
|                         // catch callback | ||||
|                         return doCatch ? asPromise(callback(value)): asPromise(value); | ||||
|                     } | ||||
|                 }; | ||||
|             } | ||||
|  | ||||
|             beforeEach(function () { | ||||
|                 happyPromise = asPromise(true); | ||||
|                 model = { someKey: "some value", name: "domain object"}; | ||||
|  | ||||
|                 mockPersistenceService = jasmine.createSpyObj( | ||||
|                     "persistenceService", | ||||
|                     [ "updateObject", "readObject", "createObject", "deleteObject" ] | ||||
|                 ); | ||||
|  | ||||
|                 mockIdentifierService = jasmine.createSpyObj( | ||||
|                     'identifierService', | ||||
|                     [ 'parse', 'generate' ] | ||||
| @@ -60,6 +73,15 @@ define( | ||||
|                     'identifier', | ||||
|                     [ 'getSpace', 'getKey', 'getDefinedSpace' ] | ||||
|                 ); | ||||
|                 mockQ = jasmine.createSpyObj( | ||||
|                     "$q", | ||||
|                     ["reject"] | ||||
|                 ); | ||||
|                 mockNofificationService = jasmine.createSpyObj( | ||||
|                     "notificationService", | ||||
|                     ["error"] | ||||
|                 ); | ||||
|  | ||||
|                 mockDomainObject = { | ||||
|                     getId: function () { return id; }, | ||||
|                     getModel: function () { return model; }, | ||||
| @@ -76,66 +98,99 @@ define( | ||||
|                 persistence = new PersistenceCapability( | ||||
|                     mockPersistenceService, | ||||
|                     mockIdentifierService, | ||||
|                     mockNofificationService, | ||||
|                     mockQ, | ||||
|                     mockDomainObject | ||||
|                 ); | ||||
|             }); | ||||
|  | ||||
|             it("creates unpersisted objects with the persistence service", function () { | ||||
|                 // Verify precondition; no call made during constructor | ||||
|                 expect(mockPersistenceService.createObject).not.toHaveBeenCalled(); | ||||
|             describe("successful persistence", function() { | ||||
|                 beforeEach(function () { | ||||
|                     mockPersistenceService.updateObject.andReturn(happyPromise); | ||||
|                     mockPersistenceService.createObject.andReturn(happyPromise); | ||||
|                 }); | ||||
|                 it("creates unpersisted objects with the persistence service", function () { | ||||
|                     // Verify precondition; no call made during constructor | ||||
|                     expect(mockPersistenceService.createObject).not.toHaveBeenCalled(); | ||||
|  | ||||
|                 persistence.persist(); | ||||
|                     persistence.persist(); | ||||
|  | ||||
|                 expect(mockPersistenceService.createObject).toHaveBeenCalledWith( | ||||
|                     SPACE, | ||||
|                     id, | ||||
|                     model | ||||
|                 ); | ||||
|                     expect(mockPersistenceService.createObject).toHaveBeenCalledWith( | ||||
|                         SPACE, | ||||
|                         id, | ||||
|                         model | ||||
|                     ); | ||||
|                 }); | ||||
|  | ||||
|                 it("updates previously persisted objects with the persistence service", function () { | ||||
|                     // Verify precondition; no call made during constructor | ||||
|                     expect(mockPersistenceService.updateObject).not.toHaveBeenCalled(); | ||||
|  | ||||
|                     model.persisted = 12321; | ||||
|                     persistence.persist(); | ||||
|  | ||||
|                     expect(mockPersistenceService.updateObject).toHaveBeenCalledWith( | ||||
|                         SPACE, | ||||
|                         id, | ||||
|                         model | ||||
|                     ); | ||||
|                 }); | ||||
|  | ||||
|                 it("reports which persistence space an object belongs to", function () { | ||||
|                     expect(persistence.getSpace()).toEqual(SPACE); | ||||
|                 }); | ||||
|  | ||||
|                 it("updates persisted timestamp on persistence", function () { | ||||
|                     model.modified = 12321; | ||||
|                     persistence.persist(); | ||||
|                     expect(model.persisted).toEqual(12321); | ||||
|                 }); | ||||
|                 it("refreshes the domain object model from persistence", function () { | ||||
|                     var refreshModel = {someOtherKey: "some other value"}; | ||||
|                     mockPersistenceService.readObject.andReturn(asPromise(refreshModel)); | ||||
|                     persistence.refresh(); | ||||
|                     expect(model).toEqual(refreshModel); | ||||
|                 }); | ||||
|  | ||||
|                 it("does not overwrite unpersisted changes on refresh", function () { | ||||
|                     var refreshModel = {someOtherKey: "some other value"}, | ||||
|                         mockCallback = jasmine.createSpy(); | ||||
|                     model.modified = 2; | ||||
|                     model.persisted = 1; | ||||
|                     mockPersistenceService.readObject.andReturn(asPromise(refreshModel)); | ||||
|                     persistence.refresh().then(mockCallback); | ||||
|                     expect(model).not.toEqual(refreshModel); | ||||
|                     // Should have also indicated that no changes were actually made | ||||
|                     expect(mockCallback).toHaveBeenCalledWith(false); | ||||
|                 }); | ||||
|  | ||||
|                 it("does not trigger error notification on successful" + | ||||
|                     " persistence", function () { | ||||
|                     persistence.persist(); | ||||
|                     expect(mockQ.reject).not.toHaveBeenCalled(); | ||||
|                     expect(mockNofificationService.error).not.toHaveBeenCalled(); | ||||
|                 }); | ||||
|             }); | ||||
|             describe("unsuccessful persistence", function() { | ||||
|                 var sadPromise = { | ||||
|                         then: function(callback){ | ||||
|                             return asPromise(callback(0), true); | ||||
|                         } | ||||
|                     }; | ||||
|                 beforeEach(function () { | ||||
|                     mockPersistenceService.createObject.andReturn(sadPromise); | ||||
|                 }); | ||||
|                 it("rejects on falsey persistence result", function () { | ||||
|                     persistence.persist(); | ||||
|                     expect(mockQ.reject).toHaveBeenCalled(); | ||||
|                 }); | ||||
|  | ||||
|             it("updates previously persisted objects with the persistence service", function () { | ||||
|                 // Verify precondition; no call made during constructor | ||||
|                 expect(mockPersistenceService.updateObject).not.toHaveBeenCalled(); | ||||
|  | ||||
|                 model.persisted = 12321; | ||||
|                 persistence.persist(); | ||||
|  | ||||
|                 expect(mockPersistenceService.updateObject).toHaveBeenCalledWith( | ||||
|                     SPACE, | ||||
|                     id, | ||||
|                     model | ||||
|                 ); | ||||
|                 it("notifies user on persistence failure", function () { | ||||
|                     persistence.persist(); | ||||
|                     expect(mockQ.reject).toHaveBeenCalled(); | ||||
|                     expect(mockNofificationService.error).toHaveBeenCalled(); | ||||
|                 }); | ||||
|             }); | ||||
|  | ||||
|             it("reports which persistence space an object belongs to", function () { | ||||
|                 expect(persistence.getSpace()).toEqual(SPACE); | ||||
|             }); | ||||
|  | ||||
|             it("updates persisted timestamp on persistence", function () { | ||||
|                 model.modified = 12321; | ||||
|                 persistence.persist(); | ||||
|                 expect(model.persisted).toEqual(12321); | ||||
|             }); | ||||
|  | ||||
|             it("refreshes the domain object model from persistence", function () { | ||||
|                 var refreshModel = { someOtherKey: "some other value" }; | ||||
|                 mockPersistenceService.readObject.andReturn(asPromise(refreshModel)); | ||||
|                 persistence.refresh(); | ||||
|                 expect(model).toEqual(refreshModel); | ||||
|             }); | ||||
|  | ||||
|             it("does not overwrite unpersisted changes on refresh", function () { | ||||
|                 var refreshModel = { someOtherKey: "some other value" }, | ||||
|                     mockCallback = jasmine.createSpy(); | ||||
|                 model.modified = 2; | ||||
|                 model.persisted = 1; | ||||
|                 mockPersistenceService.readObject.andReturn(asPromise(refreshModel)); | ||||
|                 persistence.refresh().then(mockCallback); | ||||
|                 expect(model).not.toEqual(refreshModel); | ||||
|                 // Should have also indicated that no changes were actually made | ||||
|                 expect(mockCallback).toHaveBeenCalledWith(false); | ||||
|             }); | ||||
|  | ||||
|         }); | ||||
|     } | ||||
| ); | ||||
|   | ||||
| @@ -39,6 +39,15 @@ | ||||
|                 "glyph": "\u00F4", | ||||
|                 "category": "contextual", | ||||
|                 "implementation": "actions/GoToOriginalAction.js" | ||||
|             }, | ||||
|             { | ||||
|                 "key": "locate", | ||||
|                 "name": "Set Primary Location", | ||||
|                 "description": "Set a domain object's primary location.", | ||||
|                 "glyph": "", | ||||
|                 "category": "contextual", | ||||
|                 "implementation": "actions/SetPrimaryLocationAction.js" | ||||
|  | ||||
|             } | ||||
|         ], | ||||
|         "components": [ | ||||
| @@ -89,8 +98,7 @@ | ||||
|                 "name": "Copy Service", | ||||
|                 "description": "Provides a service for copying objects", | ||||
|                 "implementation": "services/CopyService.js", | ||||
|                 "depends": ["$q", "creationService", "policyService", | ||||
|                     "persistenceService", "now"] | ||||
|                 "depends": ["$q", "policyService", "now"] | ||||
|             }, | ||||
|             { | ||||
|                 "key": "locationService", | ||||
|   | ||||
| @@ -0,0 +1,60 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT Web, Copyright (c) 2014-2015, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
|  * Administration. All rights reserved. | ||||
|  * | ||||
|  * Open MCT Web 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 Web 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. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| /*global define */ | ||||
| define( | ||||
|     function () { | ||||
|         "use strict"; | ||||
|  | ||||
|         /** | ||||
|          * Implements the "Set Primary Location" action, which sets a | ||||
|          * location property for objects to match their contextual | ||||
|          * location. | ||||
|          * | ||||
|          * @implements {Action} | ||||
|          * @constructor | ||||
|          * @private | ||||
|          * @memberof platform/entanglement | ||||
|          * @param {ActionContext} context the context in which the action | ||||
|          *        will be performed | ||||
|          */ | ||||
|         function SetPrimaryLocationAction(context) { | ||||
|             this.domainObject = context.domainObject; | ||||
|         } | ||||
|  | ||||
|         SetPrimaryLocationAction.prototype.perform = function () { | ||||
|             var location = this.domainObject.getCapability('location'); | ||||
|             return location.setPrimaryLocation( | ||||
|                 location.getContextualLocation() | ||||
|             ); | ||||
|         }; | ||||
|  | ||||
|         SetPrimaryLocationAction.appliesTo = function (context) { | ||||
|             var domainObject = context.domainObject; | ||||
|             return domainObject && domainObject.hasCapability("location") | ||||
|                 && (domainObject.getModel().location === undefined); | ||||
|         }; | ||||
|  | ||||
|         return SetPrimaryLocationAction; | ||||
|     } | ||||
| ); | ||||
|  | ||||
| @@ -28,9 +28,7 @@ define( | ||||
|  | ||||
|         var DISALLOWED_ACTIONS = [ | ||||
|             "move", | ||||
|             "copy", | ||||
|             "link", | ||||
|             "compose" | ||||
|             "copy" | ||||
|         ]; | ||||
|  | ||||
|         /** | ||||
|   | ||||
| @@ -38,12 +38,9 @@ define( | ||||
|          * @memberof platform/entanglement | ||||
|          * @implements {platform/entanglement.AbstractComposeService} | ||||
|          */ | ||||
|         function CopyService($q, creationService, policyService, persistenceService, now) { | ||||
|         function CopyService($q, policyService) { | ||||
|             this.$q = $q; | ||||
|             this.creationService = creationService; | ||||
|             this.policyService = policyService; | ||||
|             this.persistenceService = persistenceService; | ||||
|             this.now = now; | ||||
|         } | ||||
|  | ||||
|         CopyService.prototype.validate = function (object, parentCandidate) { | ||||
| @@ -71,7 +68,7 @@ define( | ||||
|          */ | ||||
|         CopyService.prototype.perform = function (domainObject, parent) { | ||||
|             var $q = this.$q, | ||||
|                 copyTask = new CopyTask(domainObject, parent, this.persistenceService, this.$q, this.now); | ||||
|                 copyTask = new CopyTask(domainObject, parent, this.policyService, this.$q); | ||||
|             if (this.validate(domainObject, parent)) { | ||||
|                 return copyTask.perform(); | ||||
|             } else { | ||||
|   | ||||
| @@ -23,8 +23,8 @@ | ||||
| /*global define */ | ||||
|  | ||||
| define( | ||||
|     ["uuid"], | ||||
|     function (uuid) { | ||||
|     [], | ||||
|     function () { | ||||
|         "use strict"; | ||||
|  | ||||
|         /** | ||||
| @@ -33,36 +33,51 @@ define( | ||||
|          * | ||||
|          * @param domainObject The object to copy | ||||
|          * @param parent The new location of the cloned object tree | ||||
|          * @param persistenceService | ||||
|          * @param $q | ||||
|          * @param now | ||||
|          * @constructor | ||||
|          */ | ||||
|         function CopyTask (domainObject, parent, persistenceService, $q, now){ | ||||
|         function CopyTask (domainObject, parent, policyService, $q){ | ||||
|             this.domainObject = domainObject; | ||||
|             this.parent = parent; | ||||
|             this.firstClone = undefined; | ||||
|             this.$q = $q; | ||||
|             this.deferred = undefined; | ||||
|             this.persistenceService = persistenceService; | ||||
|             this.policyService = policyService; | ||||
|             this.persisted = 0; | ||||
|             this.now = now; | ||||
|             this.clones = []; | ||||
|             this.idMap = {}; | ||||
|         } | ||||
|  | ||||
|         function composeChild(child, parent) { | ||||
|         function composeChild(child, parent, setLocation) { | ||||
|             //Once copied, associate each cloned | ||||
|             // composee with its parent clone | ||||
|             child.model.location = parent.id; | ||||
|             parent.model.composition = parent.model.composition || []; | ||||
|             return parent.model.composition.push(child.id); | ||||
|  | ||||
|             parent.getModel().composition.push(child.getId()); | ||||
|  | ||||
|             //If a location is not specified, set it. | ||||
|             if (setLocation && child.getModel().location === undefined) { | ||||
|                 child.getModel().location = parent.getId(); | ||||
|             } | ||||
|  | ||||
|             return child; | ||||
|         } | ||||
|  | ||||
|         function cloneObjectModel(objectModel) { | ||||
|             var clone = JSON.parse(JSON.stringify(objectModel)); | ||||
|  | ||||
|             delete clone.composition; | ||||
|             /** | ||||
|              * Reset certain fields. | ||||
|              */ | ||||
|             //If has a composition, set it to an empty array. Will be | ||||
|             // recomposed later with the ids of its cloned children. | ||||
|             if (clone.composition) { | ||||
|                 //Important to set it to an empty array here, otherwise | ||||
|                 // hasCapability("composition") returns false; | ||||
|                 clone.composition = []; | ||||
|             } | ||||
|             delete clone.persisted; | ||||
|             delete clone.modified; | ||||
|             delete clone.location; | ||||
|  | ||||
|             return clone; | ||||
|         } | ||||
| @@ -73,13 +88,10 @@ define( | ||||
|          * result in automatic request batching by the browser. | ||||
|          */ | ||||
|         function persistObjects(self) { | ||||
|  | ||||
|             return self.$q.all(self.clones.map(function(clone){ | ||||
|                 clone.model.persisted = self.now(); | ||||
|                 return self.persistenceService.createObject(clone.persistenceSpace, clone.id, clone.model) | ||||
|                     .then(function(){ | ||||
|                         self.deferred.notify({phase: "copying", totalObjects: self.clones.length, processed: ++self.persisted}); | ||||
|                     }); | ||||
|                 return clone.getCapability("persistence").persist().then(function(){ | ||||
|                     self.deferred.notify({phase: "copying", totalObjects: self.clones.length, processed: ++self.persisted}); | ||||
|                 }); | ||||
|             })).then(function(){ | ||||
|                 return self; | ||||
|             }); | ||||
| @@ -89,19 +101,40 @@ define( | ||||
|          * Will add a list of clones to the specified parent's composition | ||||
|          */ | ||||
|         function addClonesToParent(self) { | ||||
|             var parentClone = self.clones[self.clones.length-1]; | ||||
|             return self.firstClone.getCapability("persistence").persist() | ||||
|                 .then(function(){self.parent.getCapability("composition").add(self.firstClone.getId());}) | ||||
|                 .then(function(){return self.parent.getCapability("persistence").persist();}) | ||||
|                 .then(function(){return self.firstClone;}); | ||||
|         } | ||||
|  | ||||
|             if (!self.parent.hasCapability('composition')){ | ||||
|                 return self.$q.reject(); | ||||
|         /** | ||||
|          * Update identifiers in a cloned object model (or part of | ||||
|          * a cloned object model) to reflect new identifiers after | ||||
|          * copying. | ||||
|          * @private | ||||
|          */ | ||||
|         CopyTask.prototype.rewriteIdentifiers = function (obj, idMap) { | ||||
|             function lookupValue(value) { | ||||
|                 return (typeof value === 'string' && idMap[value]) || value; | ||||
|             } | ||||
|  | ||||
|             return self.persistenceService | ||||
|                 .updateObject(parentClone.persistenceSpace, parentClone.id, parentClone.model) | ||||
|                 .then(function(){return self.parent.getCapability("composition").add(parentClone.id);}) | ||||
|                 .then(function(){return self.parent.getCapability("persistence").persist();}) | ||||
|                 .then(function(){return parentClone;}); | ||||
|             // Ensure the clone of the original domainObject is returned | ||||
|         } | ||||
|             if (Array.isArray(obj)) { | ||||
|                 obj.forEach(function (value, index) { | ||||
|                     obj[index] = lookupValue(value); | ||||
|                     this.rewriteIdentifiers(obj[index], idMap); | ||||
|                 }, this); | ||||
|             } else if (obj && typeof obj === 'object') { | ||||
|                 Object.keys(obj).forEach(function (key) { | ||||
|                     var value = obj[key]; | ||||
|                     obj[key] = lookupValue(value); | ||||
|                     if (idMap[key]) { | ||||
|                         delete obj[key]; | ||||
|                         obj[idMap[key]] = value; | ||||
|                     } | ||||
|                     this.rewriteIdentifiers(value, idMap); | ||||
|                 }, this); | ||||
|             } | ||||
|         }; | ||||
|  | ||||
|         /** | ||||
|          * Given an array of objects composed by a parent, clone them, then | ||||
| @@ -110,18 +143,37 @@ define( | ||||
|          * @returns {*} | ||||
|          */ | ||||
|         CopyTask.prototype.copyComposees = function(composees, clonedParent, originalParent){ | ||||
|             var self = this; | ||||
|             var self = this, | ||||
|                 idMap = {}; | ||||
|  | ||||
|             return (composees || []).reduce(function(promise, composee){ | ||||
|             return (composees || []).reduce(function(promise, originalComposee){ | ||||
|                 //If the composee is composed of other | ||||
|                 // objects, chain a promise.. | ||||
|                 return promise.then(function(){ | ||||
|                     // ...to recursively copy it (and its children) | ||||
|                     return self.copy(composee, originalParent).then(function(composee){ | ||||
|                         composeChild(composee, clonedParent); | ||||
|                     return self.copy(originalComposee, originalParent).then(function(clonedComposee){ | ||||
|                         //Map the original composee's ID to that of its | ||||
|                         // clone so that we can replace any references to it | ||||
|                         // in the parent | ||||
|                         idMap[originalComposee.getId()] = clonedComposee.getId(); | ||||
|  | ||||
|                         //Compose the child within its parent. Cloned | ||||
|                         // objects will need to also have their location | ||||
|                         // set, however linked objects will not. | ||||
|                         return composeChild(clonedComposee, clonedParent, clonedComposee !== originalComposee); | ||||
|                     }); | ||||
|                 });}, self.$q.when(undefined) | ||||
|             ); | ||||
|             ).then(function(){ | ||||
|                     //Replace any references in the cloned parent to | ||||
|                     // contained objects that have been composed with the | ||||
|                     // Ids of the clones | ||||
|                     self.rewriteIdentifiers(clonedParent.getModel(), idMap); | ||||
|  | ||||
|                     //Add the clone to the list of clones that will | ||||
|                     //be returned by this function | ||||
|                     self.clones.push(clonedParent); | ||||
|                     return clonedParent; | ||||
|             }); | ||||
|         }; | ||||
|  | ||||
|         /** | ||||
| @@ -131,29 +183,38 @@ define( | ||||
|          * cloning objects, and composing them with their child clones | ||||
|          * as it goes | ||||
|          * @private | ||||
|          * @param originalObject | ||||
|          * @param originalParent | ||||
|          * @returns {*} | ||||
|          * @returns {DomainObject} If the type of the original object allows for | ||||
|          * duplication, then a duplicate of the object, otherwise the object | ||||
|          * itself (to allow linking to non duplicatable objects). | ||||
|          */ | ||||
|         CopyTask.prototype.copy = function(originalObject, originalParent) { | ||||
|         CopyTask.prototype.copy = function(originalObject) { | ||||
|             var self = this, | ||||
|                 modelClone = { | ||||
|                 id: uuid(), | ||||
|                 model: cloneObjectModel(originalObject.getModel()), | ||||
|                 persistenceSpace: originalParent.hasCapability('persistence') && originalParent.getCapability('persistence').getSpace() | ||||
|             }; | ||||
|                 clone; | ||||
|  | ||||
|             return this.$q.when(originalObject.useCapability('composition')).then(function(composees){ | ||||
|                 self.deferred.notify({phase: "preparing"}); | ||||
|                 //Duplicate the object's children, and their children, and | ||||
|                 // so on down to the leaf nodes of the tree. | ||||
|                 return self.copyComposees(composees, modelClone, originalObject).then(function (){ | ||||
|                     //Add the clone to the list of clones that will | ||||
|                     //be returned by this function | ||||
|                     self.clones.push(modelClone); | ||||
|                     return modelClone; | ||||
|             //Check if the type of the object being copied allows for | ||||
|             // creation of new instances. If it does not, then a link to the | ||||
|             // original will be created instead. | ||||
|             if (this.policyService.allow("creation", originalObject.getCapability("type"))){ | ||||
|                 //create a new clone of the original object. Use the | ||||
|                 // creation capability of the targetParent to create the | ||||
|                 // new clone. This will ensure that the correct persistence | ||||
|                 // space is used. | ||||
|                 clone = this.parent.useCapability("instantiation", cloneObjectModel(originalObject.getModel())); | ||||
|  | ||||
|                 //Iterate through child tree | ||||
|                 return this.$q.when(originalObject.useCapability('composition')).then(function(composees){ | ||||
|                     self.deferred.notify({phase: "preparing"}); | ||||
|                     //Duplicate the object's children, and their children, and | ||||
|                     // so on down to the leaf nodes of the tree. | ||||
|                     //If it is a link, don't both with children | ||||
|                     return self.copyComposees(composees, clone, originalObject); | ||||
|                 }); | ||||
|             }); | ||||
|             } else { | ||||
|                 //Creating a link, no need to iterate children | ||||
|                 return self.$q.when(originalObject); | ||||
|             } | ||||
|  | ||||
|  | ||||
|         }; | ||||
|  | ||||
|         /** | ||||
| @@ -172,7 +233,10 @@ define( | ||||
|             var self = this; | ||||
|  | ||||
|             return this.copy(self.domainObject, self.parent).then(function(domainObjectClone){ | ||||
|                 domainObjectClone.model.location = self.parent.getId(); | ||||
|                 if (domainObjectClone !== self.domainObject) { | ||||
|                     domainObjectClone.getModel().location = self.parent.getId(); | ||||
|                 } | ||||
|                 self.firstClone = domainObjectClone; | ||||
|                 return self; | ||||
|             }); | ||||
|         }; | ||||
|   | ||||
| @@ -0,0 +1,80 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT Web, Copyright (c) 2014-2015, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
|  * Administration. All rights reserved. | ||||
|  * | ||||
|  * Open MCT Web 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 Web 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. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| /*global define,describe,beforeEach,it,jasmine,expect */ | ||||
|  | ||||
| define( | ||||
|     [ | ||||
|         '../../src/actions/SetPrimaryLocationAction', | ||||
|         '../DomainObjectFactory' | ||||
|     ], | ||||
|     function (SetPrimaryLocation, domainObjectFactory) { | ||||
|         'use strict'; | ||||
|  | ||||
|         describe("The 'set primary location' action", function () { | ||||
|             var testContext, | ||||
|                 testModel, | ||||
|                 testId, | ||||
|                 mockLocationCapability, | ||||
|                 mockContextCapability; | ||||
|  | ||||
|             beforeEach(function () { | ||||
|                 testId = "some-id"; | ||||
|                 testModel = { name: "some name" }; | ||||
|  | ||||
|                 mockLocationCapability = jasmine.createSpyObj( | ||||
|                     'location', | ||||
|                     [ 'setPrimaryLocation', 'getContextualLocation' ] | ||||
|                 ); | ||||
|  | ||||
|                 mockLocationCapability.getContextualLocation.andReturn(testId); | ||||
|  | ||||
|                 testContext = { | ||||
|                     domainObject: domainObjectFactory({ | ||||
|                         capabilities: { | ||||
|                             location: mockLocationCapability | ||||
|                         }, | ||||
|                         model: testModel | ||||
|                     }) | ||||
|                 }; | ||||
|             }); | ||||
|  | ||||
|             it("is applicable to objects with no location specified", function () { | ||||
|                 expect(SetPrimaryLocation.appliesTo(testContext)) | ||||
|                     .toBe(true); | ||||
|                 testContext.domainObject.getModel.andReturn({ | ||||
|                     location: "something", | ||||
|                     name: "some name" | ||||
|                 }); | ||||
|                 expect(SetPrimaryLocation.appliesTo(testContext)) | ||||
|                     .toBe(false); | ||||
|             }); | ||||
|  | ||||
|             it("sets the location contextually when performed", function () { | ||||
|                 new SetPrimaryLocation(testContext).perform(); | ||||
|                 expect(mockLocationCapability.setPrimaryLocation) | ||||
|                     .toHaveBeenCalledWith(testId); | ||||
|             }); | ||||
|  | ||||
|         }); | ||||
|     } | ||||
| ); | ||||
| @@ -72,7 +72,7 @@ define( | ||||
|                 policy = new CrossSpacePolicy(); | ||||
|             }); | ||||
|  | ||||
|             ['move', 'copy', 'link', 'compose'].forEach(function (key) { | ||||
|             ['move', 'copy'].forEach(function (key) { | ||||
|                 describe("for " + key + " actions", function () { | ||||
|                     beforeEach(function () { | ||||
|                         testActionMetadata.key = key; | ||||
|   | ||||
| @@ -63,7 +63,6 @@ define( | ||||
|  | ||||
|                 beforeEach(function () { | ||||
|                     copyService = new CopyService( | ||||
|                         null, | ||||
|                         null, | ||||
|                         policyService | ||||
|                     ); | ||||
| @@ -130,47 +129,50 @@ define( | ||||
|                     creationService, | ||||
|                     createObjectPromise, | ||||
|                     copyService, | ||||
|                     mockPersistenceService, | ||||
|                     mockNow, | ||||
|                     object, | ||||
|                     newParent, | ||||
|                     copyResult, | ||||
|                     copyFinished, | ||||
|                     persistObjectPromise, | ||||
|                     parentPersistenceCapability, | ||||
|                     persistenceCapability, | ||||
|                     instantiationCapability, | ||||
|                     compositionCapability, | ||||
|                     locationCapability, | ||||
|                     resolvedValue; | ||||
|  | ||||
|                 beforeEach(function () { | ||||
|                     creationService = jasmine.createSpyObj( | ||||
|                         'creationService', | ||||
|                         ['createObject'] | ||||
|                     ); | ||||
|                     createObjectPromise = synchronousPromise(undefined); | ||||
|                     creationService.createObject.andReturn(createObjectPromise); | ||||
|                     policyService.allow.andReturn(true); | ||||
|                      | ||||
|                     mockPersistenceService = jasmine.createSpyObj( | ||||
|                         'persistenceService', | ||||
|                         ['createObject', 'updateObject'] | ||||
|                     ); | ||||
|  | ||||
|                     persistObjectPromise = synchronousPromise(undefined); | ||||
|                     mockPersistenceService.createObject.andReturn(persistObjectPromise); | ||||
|                     mockPersistenceService.updateObject.andReturn(persistObjectPromise); | ||||
|                      | ||||
|                     parentPersistenceCapability = jasmine.createSpyObj( | ||||
|                         "persistence", | ||||
|  | ||||
|                     instantiationCapability = jasmine.createSpyObj( | ||||
|                         "instantiation", | ||||
|                         [ "invoke" ] | ||||
|                     ); | ||||
|  | ||||
|                     persistenceCapability = jasmine.createSpyObj( | ||||
|                         "persistenceCapability", | ||||
|                         [ "persist", "getSpace" ] | ||||
|                     ); | ||||
|                     persistenceCapability.persist.andReturn(persistObjectPromise); | ||||
|  | ||||
|                     parentPersistenceCapability.persist.andReturn(persistObjectPromise); | ||||
|                     parentPersistenceCapability.getSpace.andReturn("testSpace"); | ||||
|                     compositionCapability = jasmine.createSpyObj( | ||||
|                         'compositionCapability', | ||||
|                         ['invoke', 'add'] | ||||
|                     ); | ||||
|  | ||||
|                     mockNow = jasmine.createSpyObj("mockNow", ["now"]); | ||||
|                     mockNow.now.andCallFake(function(){ | ||||
|                         return 1234; | ||||
|                     }); | ||||
|                     locationCapability = jasmine.createSpyObj( | ||||
|                         'locationCapability', | ||||
|                         ['isLink'] | ||||
|                     ); | ||||
|                     locationCapability.isLink.andReturn(false); | ||||
|  | ||||
|                     mockDeferred = jasmine.createSpyObj('mockDeferred', ['notify', 'resolve']); | ||||
|                     mockDeferred = jasmine.createSpyObj( | ||||
|                         'mockDeferred', | ||||
|                         ['notify', 'resolve', 'reject'] | ||||
|                     ); | ||||
|                     mockDeferred.notify.andCallFake(function(notification){}); | ||||
|                     mockDeferred.resolve.andCallFake(function(value){resolvedValue = value;}); | ||||
|                     mockDeferred.promise = { | ||||
| @@ -179,7 +181,11 @@ define( | ||||
|                         } | ||||
|                     }; | ||||
|  | ||||
|                     mockQ = jasmine.createSpyObj('mockQ', ['when', 'all', 'reject', 'defer']); | ||||
|                     mockQ = jasmine.createSpyObj( | ||||
|                         'mockQ', | ||||
|                         ['when', 'all', 'reject', 'defer'] | ||||
|                     ); | ||||
|                     mockQ.reject.andReturn(synchronousPromise(undefined)); | ||||
|                     mockQ.when.andCallFake(synchronousPromise); | ||||
|                     mockQ.all.andCallFake(function (promises) { | ||||
|                         var result = {}; | ||||
| @@ -194,6 +200,8 @@ define( | ||||
|  | ||||
|                 describe("on domain object without composition", function () { | ||||
|                     beforeEach(function () { | ||||
|                         var objectCopy; | ||||
|  | ||||
|                         newParent = domainObjectFactory({ | ||||
|                             name: 'newParent', | ||||
|                             id: '456', | ||||
| @@ -201,7 +209,9 @@ define( | ||||
|                                 composition: [] | ||||
|                             }, | ||||
|                             capabilities: { | ||||
|                                 persistence: parentPersistenceCapability | ||||
|                                 instantiation: instantiationCapability, | ||||
|                                 persistence: persistenceCapability, | ||||
|                                 composition: compositionCapability | ||||
|                             } | ||||
|                         }); | ||||
|  | ||||
| @@ -210,31 +220,46 @@ define( | ||||
|                             id: 'abc', | ||||
|                             model: { | ||||
|                                 name: 'some object', | ||||
|                                 location: newParent.id, | ||||
|                                 persisted: mockNow.now() | ||||
|                                 location: '456', | ||||
|                                 someOtherAttribute: 'some other value', | ||||
|                                 embeddedObjectAttribute: { | ||||
|                                     name: 'Some embedded object' | ||||
|                                 } | ||||
|                             }, | ||||
|                             capabilities: { | ||||
|                                 persistence: persistenceCapability | ||||
|                             } | ||||
|                         }); | ||||
|                          | ||||
|                         copyService = new CopyService(mockQ, creationService, policyService, mockPersistenceService, mockNow.now); | ||||
|  | ||||
|                         objectCopy = domainObjectFactory({ | ||||
|                             name: 'object', | ||||
|                             id: 'abc.copy.fdgdfgdf', | ||||
|                             capabilities: { | ||||
|                                 persistence: persistenceCapability, | ||||
|                                 location: locationCapability | ||||
|                             } | ||||
|                         }); | ||||
|  | ||||
|                         instantiationCapability.invoke.andCallFake( | ||||
|                             function(model){ | ||||
|                                 objectCopy.model = model; | ||||
|                                 return objectCopy; | ||||
|                             } | ||||
|                         ); | ||||
|  | ||||
|                         copyService = new CopyService(mockQ, policyService); | ||||
|                         copyResult = copyService.perform(object, newParent); | ||||
|                         copyFinished = jasmine.createSpy('copyFinished'); | ||||
|                         copyResult.then(copyFinished); | ||||
|                     }); | ||||
|  | ||||
|                     it("uses persistence service", function () { | ||||
|                      expect(mockPersistenceService.createObject) | ||||
|                      .toHaveBeenCalledWith(parentPersistenceCapability.getSpace(), jasmine.any(String), object.getModel()); | ||||
|  | ||||
|                      expect(persistObjectPromise.then) | ||||
|                      .toHaveBeenCalledWith(jasmine.any(Function)); | ||||
|                      }); | ||||
|                     it("uses persistence capability", function () { | ||||
|                         expect(persistenceCapability.persist) | ||||
|                             .toHaveBeenCalled(); | ||||
|                     }); | ||||
|                      | ||||
|                     it("deep clones object model", function () { | ||||
|                         //var newModel = creationService | ||||
|                         var newModel = mockPersistenceService | ||||
|                             .createObject | ||||
|                             .mostRecentCall | ||||
|                             .args[2]; | ||||
|                         var newModel = copyFinished.calls[0].args[0].getModel(); | ||||
|                         expect(newModel).toEqual(object.model); | ||||
|                         expect(newModel).not.toBe(object.model); | ||||
|                     }); | ||||
| @@ -249,27 +274,57 @@ define( | ||||
|                 describe("on domainObject with composition", function () { | ||||
|                     var newObject, | ||||
|                         childObject, | ||||
|                         compositionCapability, | ||||
|                         locationCapability, | ||||
|                         objectClone, | ||||
|                         childObjectClone, | ||||
|                         compositionPromise; | ||||
|  | ||||
|                     beforeEach(function () { | ||||
|                         var invocationCount = 0, | ||||
|                             objectClones; | ||||
|  | ||||
|                         instantiationCapability.invoke.andCallFake( | ||||
|                             function(model){ | ||||
|                                 var cloneToReturn = objectClones[invocationCount++]; | ||||
|                                 cloneToReturn.model = model; | ||||
|                                 return cloneToReturn; | ||||
|                             } | ||||
|                         ); | ||||
|  | ||||
|                         locationCapability = jasmine.createSpyObj('locationCapability', ['isLink']); | ||||
|                         locationCapability.isLink.andReturn(true); | ||||
|                         newParent = domainObjectFactory({ | ||||
|                             name: 'newParent', | ||||
|                             id: '456', | ||||
|                             model: { | ||||
|                                 composition: [] | ||||
|                             }, | ||||
|                             capabilities: { | ||||
|                                 instantiation: instantiationCapability, | ||||
|                                 persistence: persistenceCapability, | ||||
|                                 composition: compositionCapability | ||||
|                             } | ||||
|                         }); | ||||
|  | ||||
|                         childObject = domainObjectFactory({ | ||||
|                             name: 'childObject', | ||||
|                             id: 'def', | ||||
|                             model: { | ||||
|                                 name: 'a child object' | ||||
|                                 name: 'a child object', | ||||
|                                 location: 'abc' | ||||
|                             }, | ||||
|                             capabilities: { | ||||
|                                 persistence: persistenceCapability, | ||||
|                                 location: locationCapability | ||||
|                             } | ||||
|                         }); | ||||
|                         compositionCapability = jasmine.createSpyObj( | ||||
|                             'compositionCapability', | ||||
|                             ['invoke', 'add'] | ||||
|                         ); | ||||
|  | ||||
|                         childObjectClone = domainObjectFactory({ | ||||
|                             name: 'childObject', | ||||
|                             id: 'def.clone', | ||||
|                             capabilities: { | ||||
|                                 persistence: persistenceCapability, | ||||
|                                 location: locationCapability | ||||
|                             } | ||||
|                         }); | ||||
|  | ||||
|                         compositionPromise = jasmine.createSpyObj( | ||||
|                             'compositionPromise', | ||||
|                             ['then'] | ||||
| @@ -280,7 +335,7 @@ define( | ||||
|                             .andReturn(synchronousPromise([childObject])); | ||||
|  | ||||
|                         object = domainObjectFactory({ | ||||
|                             name: 'object', | ||||
|                             name: 'some object', | ||||
|                             id: 'abc', | ||||
|                             model: { | ||||
|                                 name: 'some object', | ||||
| @@ -288,36 +343,27 @@ define( | ||||
|                                 location: 'testLocation' | ||||
|                             }, | ||||
|                             capabilities: { | ||||
|                                 instantiation: instantiationCapability, | ||||
|                                 composition: compositionCapability, | ||||
|                                 location: locationCapability | ||||
|                             } | ||||
|                         }); | ||||
|                         newObject = domainObjectFactory({ | ||||
|                             name: 'object', | ||||
|                             id: 'abc2', | ||||
|                             model: { | ||||
|                                 name: 'some object', | ||||
|                                 composition: [] | ||||
|                             }, | ||||
|                             capabilities: { | ||||
|                                 composition: compositionCapability | ||||
|                             } | ||||
|                         }); | ||||
|                         newParent = domainObjectFactory({ | ||||
|                             name: 'newParent', | ||||
|                             id: '456', | ||||
|                             model: { | ||||
|                                 composition: [] | ||||
|                             }, | ||||
|                             capabilities: { | ||||
|                                 composition: compositionCapability, | ||||
|                                 persistence: parentPersistenceCapability | ||||
|                                 location: locationCapability, | ||||
|                                 persistence: persistenceCapability | ||||
|                             } | ||||
|                         }); | ||||
|  | ||||
|                         createObjectPromise = synchronousPromise(newObject); | ||||
|                         creationService.createObject.andReturn(createObjectPromise); | ||||
|                         copyService = new CopyService(mockQ, creationService, policyService, mockPersistenceService, mockNow.now); | ||||
|                         objectClone = domainObjectFactory({ | ||||
|                             name: 'some object', | ||||
|                             id: 'abc.clone', | ||||
|                             capabilities: { | ||||
|                                 instantiation: instantiationCapability, | ||||
|                                 composition: compositionCapability, | ||||
|                                 location: locationCapability, | ||||
|                                 persistence: persistenceCapability | ||||
|                             } | ||||
|                         }); | ||||
|  | ||||
|                         objectClones = [objectClone, childObjectClone]; | ||||
|  | ||||
|                         copyService = new CopyService(mockQ, policyService); | ||||
|                     }); | ||||
|  | ||||
|                     describe("the cloning process", function(){ | ||||
| @@ -327,10 +373,9 @@ define( | ||||
|                             copyResult.then(copyFinished); | ||||
|                         }); | ||||
|  | ||||
|                         it("copies object and children in a bottom-up" + | ||||
|                             " fashion", function () { | ||||
|                             expect(mockPersistenceService.createObject.calls[0].args[2].name).toEqual(childObject.model.name); | ||||
|                             expect(mockPersistenceService.createObject.calls[1].args[2].name).toEqual(object.model.name); | ||||
|                         it("returns a promise", function () { | ||||
|                             expect(copyResult.then).toBeDefined(); | ||||
|                             expect(copyFinished).toHaveBeenCalled(); | ||||
|                         }); | ||||
|  | ||||
|                         it("returns a promise", function () { | ||||
| @@ -338,15 +383,27 @@ define( | ||||
|                             expect(copyFinished).toHaveBeenCalled(); | ||||
|                         }); | ||||
|  | ||||
|                         it("clears modified and sets persisted", function () { | ||||
|                             expect(copyFinished.mostRecentCall.args[0].model.modified).toBeUndefined(); | ||||
|                             expect(copyFinished.mostRecentCall.args[0].model.persisted).toBe(mockNow.now()); | ||||
|                         }); | ||||
|  | ||||
|                         it ("correctly locates cloned objects", function() { | ||||
|                             expect(mockPersistenceService.createObject.calls[0].args[2].location).toEqual(mockPersistenceService.createObject.calls[1].args[1]); | ||||
|                             expect(childObjectClone.getModel().location).toEqual(objectClone.getId()); | ||||
|                         }); | ||||
|                     }); | ||||
|                     describe("when cloning non-creatable objects", function() { | ||||
|                         beforeEach(function () { | ||||
|                             policyService.allow.andCallFake(function(category){ | ||||
|                                 //Return false for 'creation' policy | ||||
|                                return category !== 'creation'; | ||||
|                             }); | ||||
|  | ||||
|                             copyResult = copyService.perform(object, newParent); | ||||
|                             copyFinished = jasmine.createSpy('copyFinished'); | ||||
|                             copyResult.then(copyFinished); | ||||
|                         }); | ||||
|                         it ("creates link instead of clone", function() { | ||||
|                             var copiedObject = copyFinished.calls[0].args[0]; | ||||
|                             expect(copiedObject).toBe(object); | ||||
|                             expect(compositionCapability.add).toHaveBeenCalledWith(copiedObject.getId()); | ||||
|                             //expect(newParent.getModel().composition).toContain(copiedObject.getId()); | ||||
|                         }); | ||||
|                     }); | ||||
|                 }); | ||||
|  | ||||
| @@ -355,20 +412,28 @@ define( | ||||
|                         object = domainObjectFactory({ | ||||
|                             name: 'object', | ||||
|                             capabilities: { | ||||
|                                 type: { type: 'object' } | ||||
|                                 type: { type: 'object' }, | ||||
|                                 location: locationCapability, | ||||
|                                 persistence: persistenceCapability | ||||
|                             } | ||||
|                         }); | ||||
|  | ||||
|                         newParent = domainObjectFactory({ | ||||
|                             name: 'parentCandidate', | ||||
|                             capabilities: { | ||||
|                                 type: { type: 'parentCandidate' } | ||||
|                                 type: { type: 'parentCandidate' }, | ||||
|                                 instantiation: instantiationCapability, | ||||
|                                 composition: compositionCapability, | ||||
|                                 persistence: persistenceCapability | ||||
|                             } | ||||
|                         }); | ||||
|  | ||||
|                         instantiationCapability.invoke.andReturn(object); | ||||
|                     }); | ||||
|  | ||||
|                     it("throws an error", function () { | ||||
|                         var copyService = | ||||
|                             new CopyService(mockQ, creationService, policyService, mockPersistenceService, mockNow.now); | ||||
|                             new CopyService(mockQ, policyService); | ||||
|  | ||||
|                         function perform() { | ||||
|                             copyService.perform(object, newParent); | ||||
|   | ||||
							
								
								
									
										277
									
								
								platform/entanglement/test/services/CopyTaskSpec.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										277
									
								
								platform/entanglement/test/services/CopyTaskSpec.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,277 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT Web, Copyright (c) 2014-2015, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
|  * Administration. All rights reserved. | ||||
|  * | ||||
|  * Open MCT Web 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 Web 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. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| /*global define,describe,beforeEach,it,jasmine,expect,spyOn */ | ||||
|  | ||||
| define( | ||||
|     [ | ||||
|         '../../src/services/CopyTask', | ||||
|         '../DomainObjectFactory' | ||||
|     ], | ||||
|     function (CopyTask, domainObjectFactory) { | ||||
|         'use strict'; | ||||
|  | ||||
|         var ID_A = "some-string-with-vaguely-uuidish-uniqueness", | ||||
|             ID_B = "some-other-similarly-unique-string"; | ||||
|  | ||||
|         function synchronousPromise(value) { | ||||
|             return (value && value.then) ? value : { | ||||
|                 then: function (callback) { | ||||
|                     return synchronousPromise(callback(value)); | ||||
|                 } | ||||
|             }; | ||||
|         } | ||||
|  | ||||
|         describe("CopyTask", function () { | ||||
|             var mockDomainObject, | ||||
|                 mockParentObject, | ||||
|                 mockPolicyService, | ||||
|                 mockQ, | ||||
|                 mockDeferred, | ||||
|                 testModel, | ||||
|                 mockCallback, | ||||
|                 counter, | ||||
|                 cloneIds, | ||||
|                 task; | ||||
|  | ||||
|             function makeMockCapabilities(childIds) { | ||||
|                 var mockCapabilities = { | ||||
|                         persistence: jasmine.createSpyObj( | ||||
|                             'persistence', | ||||
|                             ['persist'] | ||||
|                         ), | ||||
|                         composition: jasmine.createSpyObj( | ||||
|                             'composition', | ||||
|                             ['add', 'invoke'] | ||||
|                         ), | ||||
|                         instantiation: jasmine.createSpyObj( | ||||
|                             'instantiation', | ||||
|                             ['instantiate', 'invoke'] | ||||
|                         ) | ||||
|                     }, | ||||
|                     mockChildren = (childIds || []).map(function (id) { | ||||
|                         return domainObjectFactory({ | ||||
|                             id: id, | ||||
|                             capabilities: makeMockCapabilities([]), | ||||
|                             model: { originalId: id } | ||||
|                         }); | ||||
|                     }); | ||||
|  | ||||
|                 mockCapabilities.persistence.persist | ||||
|                     .andReturn(synchronousPromise(true)); | ||||
|                 mockCapabilities.composition.add.andCallFake(function (obj) { | ||||
|                     return synchronousPromise(obj); | ||||
|                 }); | ||||
|                 mockCapabilities.composition.invoke | ||||
|                     .andReturn(synchronousPromise(mockChildren)); | ||||
|                 mockCapabilities.instantiation.invoke | ||||
|                     .andCallFake(function (model) { | ||||
|                         var id = "some-id-" + counter; | ||||
|                         cloneIds[model.originalId] = id; | ||||
|                         counter += 1; | ||||
|                         return domainObjectFactory({ | ||||
|                             id: id, | ||||
|                             model: model, | ||||
|                             capabilities: makeMockCapabilities() | ||||
|                         }); | ||||
|                     }); | ||||
|  | ||||
|                 return mockCapabilities; | ||||
|             } | ||||
|  | ||||
|             beforeEach(function () { | ||||
|                 counter = 0; | ||||
|                 cloneIds = {}; | ||||
|  | ||||
|                 testModel = { | ||||
|                     composition: [ ID_A, ID_B ], | ||||
|                     someObj: {}, | ||||
|                     someArr: [ ID_A, ID_B ], | ||||
|                     objArr: [{"id": ID_A}, {"id": ID_B}], | ||||
|                     singleElementArr: [ ID_A ] | ||||
|                 }; | ||||
|                 testModel.someObj[ID_A] = "some value"; | ||||
|                 testModel.someObj.someProperty = ID_B; | ||||
|  | ||||
|                 mockDomainObject = domainObjectFactory({ | ||||
|                     capabilities: makeMockCapabilities(testModel.composition), | ||||
|                     model: testModel | ||||
|                 }); | ||||
|                 mockParentObject = domainObjectFactory({ | ||||
|                     capabilities: makeMockCapabilities() | ||||
|                 }); | ||||
|                 mockPolicyService = jasmine.createSpyObj( | ||||
|                     'policyService', | ||||
|                     [ 'allow' ] | ||||
|                 ); | ||||
|                 mockQ = jasmine.createSpyObj('$q', ['when', 'defer', 'all']); | ||||
|                 mockDeferred = jasmine.createSpyObj( | ||||
|                     'deferred', | ||||
|                     [ 'notify', 'resolve', 'reject' ] | ||||
|                 ); | ||||
|  | ||||
|                 mockPolicyService.allow.andReturn(true); | ||||
|  | ||||
|                 mockQ.when.andCallFake(synchronousPromise); | ||||
|                 mockQ.defer.andReturn(mockDeferred); | ||||
|                 mockQ.all.andCallFake(function (promises) { | ||||
|                     return synchronousPromise(promises.map(function (promise) { | ||||
|                         var value; | ||||
|                         promise.then(function (v) { value = v; }); | ||||
|                         return value; | ||||
|                     })); | ||||
|                 }); | ||||
|  | ||||
|                 mockDeferred.resolve.andCallFake(function (value) { | ||||
|                     mockDeferred.promise = synchronousPromise(value); | ||||
|                 }); | ||||
|  | ||||
|  | ||||
|             }); | ||||
|  | ||||
|  | ||||
|             describe("produces models which", function () { | ||||
|                 var model; | ||||
|  | ||||
|                 beforeEach(function () { | ||||
|                     task = new CopyTask( | ||||
|                         mockDomainObject, | ||||
|                         mockParentObject, | ||||
|                         mockPolicyService, | ||||
|                         mockQ | ||||
|                     ); | ||||
|  | ||||
|                     task.perform().then(function (clone) { | ||||
|                         model = clone.getModel(); | ||||
|                     }); | ||||
|                 }); | ||||
|  | ||||
|                 it("contain rewritten identifiers in arrays", function () { | ||||
|                     expect(model.someArr) | ||||
|                         .toEqual(testModel.someArr.map(function (id) { | ||||
|                             return cloneIds[id]; | ||||
|                         })); | ||||
|                 }); | ||||
|  | ||||
|                 it("contain rewritten identifiers in properties", function () { | ||||
|                     expect(model.someObj.someProperty) | ||||
|                         .toEqual(cloneIds[testModel.someObj.someProperty]); | ||||
|                 }); | ||||
|  | ||||
|  | ||||
|                 it("contain rewritten identifiers in property names", function () { | ||||
|                     expect(model.someObj[cloneIds[ID_A]]) | ||||
|                         .toEqual(testModel.someObj[ID_A]); | ||||
|                 }); | ||||
|  | ||||
|                 it("contain rewritten identifiers in single-element arrays", function () { | ||||
|                     expect(model.singleElementArr) | ||||
|                         .toEqual(testModel.singleElementArr.map(function (id) { | ||||
|                             return cloneIds[id]; | ||||
|                         })); | ||||
|                 }); | ||||
|             }); | ||||
|  | ||||
|             describe("copies object trees with multiple references to the" + | ||||
|                 " same object", function () { | ||||
|                 var model, | ||||
|                     mockDomainObjectB, | ||||
|                     mockComposingObject, | ||||
|                     composingObjectModel, | ||||
|                     domainObjectClone, | ||||
|                     domainObjectBClone; | ||||
|  | ||||
|                 beforeEach(function () { | ||||
|                     mockDomainObjectB = domainObjectFactory({ | ||||
|                         capabilities: makeMockCapabilities(testModel.composition), | ||||
|                         model: testModel | ||||
|                     }); | ||||
|                     composingObjectModel = { | ||||
|                         name: 'mockComposingObject', | ||||
|                         composition: [mockDomainObject.getId(), mockDomainObjectB.getId()] | ||||
|                     }; | ||||
|                     mockComposingObject = domainObjectFactory({ | ||||
|                         capabilities: makeMockCapabilities(composingObjectModel.composition), | ||||
|                         model: composingObjectModel | ||||
|                     }); | ||||
|  | ||||
|                     mockComposingObject.capabilities.composition.invoke.andReturn([mockDomainObject, mockDomainObjectB]); | ||||
|                     task = new CopyTask( | ||||
|                         mockComposingObject, | ||||
|                         mockParentObject, | ||||
|                         mockPolicyService, | ||||
|                         mockQ | ||||
|                     ); | ||||
|  | ||||
|                     task.perform(); | ||||
|                     domainObjectClone = task.clones[2]; | ||||
|                     domainObjectBClone = task.clones[5]; | ||||
|                 }); | ||||
|  | ||||
|                 /** | ||||
|                  * mockDomainObject and mockDomainObjectB have the same | ||||
|                  * model with references to children ID_A and ID_B. Expect | ||||
|                  * that after duplication the references should differ | ||||
|                  * because they are each now referencing different child | ||||
|                  * objects. This tests the issue reported in #428 | ||||
|                  */ | ||||
|                 it(" and correctly updates child identifiers in models ", function () { | ||||
|                     var childA_ID = task.clones[0].getId(), | ||||
|                         childB_ID = task.clones[1].getId(), | ||||
|                         childC_ID = task.clones[3].getId(), | ||||
|                         childD_ID = task.clones[4].getId(); | ||||
|  | ||||
|                     expect(domainObjectClone.model.someArr[0]).toNotBe(domainObjectBClone.model.someArr[0]); | ||||
|                     expect(domainObjectClone.model.someArr[0]).toBe(childA_ID); | ||||
|                     expect(domainObjectBClone.model.someArr[0]).toBe(childC_ID); | ||||
|                     expect(domainObjectClone.model.someArr[1]).toNotBe(domainObjectBClone.model.someArr[1]); | ||||
|                     expect(domainObjectClone.model.someArr[1]).toBe(childB_ID); | ||||
|                     expect(domainObjectBClone.model.someArr[1]).toBe(childD_ID); | ||||
|                     expect(domainObjectClone.model.someObj.someProperty).toNotBe(domainObjectBClone.model.someObj.someProperty); | ||||
|                     expect(domainObjectClone.model.someObj.someProperty).toBe(childB_ID); | ||||
|                     expect(domainObjectBClone.model.someObj.someProperty).toBe(childD_ID); | ||||
|  | ||||
|                 }); | ||||
|  | ||||
|                 /** | ||||
|                  * This a bug found in testathon when testing issue #428 | ||||
|                  */ | ||||
|                 it(" and correctly updates child identifiers in object" + | ||||
|                     " arrays within models ", function () { | ||||
|                     var childA_ID = task.clones[0].getId(), | ||||
|                         childB_ID = task.clones[1].getId(), | ||||
|                         childC_ID = task.clones[3].getId(), | ||||
|                         childD_ID = task.clones[4].getId(); | ||||
|  | ||||
|                     expect(domainObjectClone.model.objArr[0].id).not.toBe(ID_A); | ||||
|                     expect(domainObjectClone.model.objArr[0].id).toBe(childA_ID); | ||||
|                     expect(domainObjectClone.model.objArr[1].id).not.toBe(ID_B); | ||||
|                     expect(domainObjectClone.model.objArr[1].id).toBe(childB_ID); | ||||
|  | ||||
|                 }); | ||||
|             }); | ||||
|  | ||||
|         }); | ||||
|  | ||||
|  | ||||
|     } | ||||
| ); | ||||
| @@ -4,8 +4,10 @@ | ||||
|     "actions/GoToOriginalAction", | ||||
|     "actions/LinkAction", | ||||
|     "actions/MoveAction", | ||||
|     "actions/SetPrimaryLocationAction", | ||||
|     "policies/CrossSpacePolicy", | ||||
|     "services/CopyService", | ||||
|     "services/CopyTask", | ||||
|     "services/LinkService", | ||||
|     "services/MoveService", | ||||
|     "services/LocationService", | ||||
|   | ||||
| @@ -42,7 +42,7 @@ define( | ||||
|                     return "C"; | ||||
|                 }, | ||||
|                 getGlyphClass: function () { | ||||
|                     return ""; | ||||
|                     return "no-icon no-collapse float-right subtle"; | ||||
|                 }, | ||||
|                 getText: function () { | ||||
|                     return text; | ||||
|   | ||||
| @@ -9,7 +9,7 @@ | ||||
|                 "glyph": "L", | ||||
|                 "type": "layout", | ||||
|                 "templateUrl": "templates/layout.html", | ||||
|                 "uses": [ "composition" ], | ||||
|                 "uses": [], | ||||
|                 "gestures": [ "drop" ] | ||||
|             }, | ||||
|             { | ||||
|   | ||||
| @@ -45,43 +45,8 @@ define( | ||||
|          * @param {Scope} $scope the controller's Angular scope | ||||
|          */ | ||||
|         function LayoutController($scope) { | ||||
|             var self = this; | ||||
|  | ||||
|             // Utility function to copy raw positions from configuration, | ||||
|             // without writing directly to configuration (to avoid triggering | ||||
|             // persistence from watchers during drags). | ||||
|             function shallowCopy(obj, keys) { | ||||
|                 var copy = {}; | ||||
|                 keys.forEach(function (k) { | ||||
|                     copy[k] = obj[k]; | ||||
|                 }); | ||||
|                 return copy; | ||||
|             } | ||||
|  | ||||
|             // Compute panel positions based on the layout's object model | ||||
|             function lookupPanels(ids) { | ||||
|                 var configuration = $scope.configuration || {}; | ||||
|  | ||||
|                 // ids is read from model.composition and may be undefined; | ||||
|                 // fall back to an array if that occurs | ||||
|                 ids = ids || []; | ||||
|  | ||||
|                 // Pull panel positions from configuration | ||||
|                 self.rawPositions = | ||||
|                     shallowCopy(configuration.panels || {}, ids); | ||||
|  | ||||
|                 // Clear prior computed positions | ||||
|                 self.positions = {}; | ||||
|  | ||||
|                 // Update width/height that we are tracking | ||||
|                 self.gridSize = | ||||
|                     ($scope.model || {}).layoutGrid || DEFAULT_GRID_SIZE; | ||||
|  | ||||
|                 // Compute positions and add defaults where needed | ||||
|                 ids.forEach(function (id, index) { | ||||
|                     self.populatePosition(id, index); | ||||
|                 }); | ||||
|             } | ||||
|             var self = this, | ||||
|                 callbackCount = 0; | ||||
|  | ||||
|             // Update grid size when it changed | ||||
|             function updateGridSize(layoutGrid) { | ||||
| @@ -92,7 +57,7 @@ define( | ||||
|                 // Only update panel positions if this actually changed things | ||||
|                 if (self.gridSize[0] !== oldSize[0] || | ||||
|                         self.gridSize[1] !== oldSize[1]) { | ||||
|                     lookupPanels(Object.keys(self.positions)); | ||||
|                     self.layoutPanels(Object.keys(self.positions)); | ||||
|                 } | ||||
|             } | ||||
|  | ||||
| @@ -120,6 +85,8 @@ define( | ||||
|                     $scope.commit("Dropped a frame."); | ||||
|                 } | ||||
|                 // Populate template-facing position for this id | ||||
|                 self.rawPositions[id] = | ||||
|                     $scope.configuration.panels[id]; | ||||
|                 self.populatePosition(id); | ||||
|                 // Layout may contain embedded views which will | ||||
|                 // listen for drops, so call preventDefault() so | ||||
| @@ -127,6 +94,28 @@ define( | ||||
|                 e.preventDefault(); | ||||
|             } | ||||
|  | ||||
|             //Will fetch fully contextualized composed objects, and populate | ||||
|             // scope with them. | ||||
|             function refreshComposition() { | ||||
|                 //Keep a track of how many composition callbacks have been made | ||||
|                 var thisCount = ++callbackCount; | ||||
|  | ||||
|                 $scope.domainObject.useCapability('composition').then(function(composition){ | ||||
|                     var ids; | ||||
|  | ||||
|                     //Is this callback for the most recent composition | ||||
|                     // request? If not, discard it. Prevents race condition | ||||
|                     if (thisCount === callbackCount){ | ||||
|                         ids = composition.map(function (object) { | ||||
|                                 return object.getId(); | ||||
|                             }) || []; | ||||
|  | ||||
|                         $scope.composition = composition; | ||||
|                         self.layoutPanels(ids); | ||||
|                     } | ||||
|                 }); | ||||
|             } | ||||
|  | ||||
|             // End drag; we don't want to put $scope into this | ||||
|             // because it triggers "cpws" (copy window or scope) | ||||
|             // errors in Angular. | ||||
| @@ -156,8 +145,8 @@ define( | ||||
|             // Watch for changes to the grid size in the model | ||||
|             $scope.$watch("model.layoutGrid", updateGridSize); | ||||
|  | ||||
|             // Position panes when the model field changes | ||||
|             $scope.$watch("model.composition", lookupPanels); | ||||
|             // Update composed objects on screen, and position panes | ||||
|             $scope.$watchCollection("model.composition", refreshComposition); | ||||
|  | ||||
|             // Position panes where they are dropped | ||||
|             $scope.$on("mctDrop", handleDrop); | ||||
| @@ -263,6 +252,43 @@ define( | ||||
|             } | ||||
|         }; | ||||
|  | ||||
|         // Utility function to copy raw positions from configuration, | ||||
|         // without writing directly to configuration (to avoid triggering | ||||
|         // persistence from watchers during drags). | ||||
|         function shallowCopy(obj, keys) { | ||||
|             var copy = {}; | ||||
|             keys.forEach(function (k) { | ||||
|                 copy[k] = obj[k]; | ||||
|             }); | ||||
|             return copy; | ||||
|         } | ||||
|  | ||||
|         /** | ||||
|          * Compute panel positions based on the layout's object model. | ||||
|          * Defined as member function to facilitate testing. | ||||
|          * @private | ||||
|          */ | ||||
|         LayoutController.prototype.layoutPanels = function (ids) { | ||||
|             var configuration = this.$scope.configuration || {}, | ||||
|                 self = this; | ||||
|  | ||||
|             // Pull panel positions from configuration | ||||
|             this.rawPositions = | ||||
|                 shallowCopy(configuration.panels || {}, ids); | ||||
|  | ||||
|             // Clear prior computed positions | ||||
|             this.positions = {}; | ||||
|  | ||||
|             // Update width/height that we are tracking | ||||
|             this.gridSize = | ||||
|                 (this.$scope.model || {}).layoutGrid || DEFAULT_GRID_SIZE; | ||||
|  | ||||
|             // Compute positions and add defaults where needed | ||||
|             ids.forEach(function (id, index) { | ||||
|                 self.populatePosition(id, index); | ||||
|             }); | ||||
|         }; | ||||
|  | ||||
|         /** | ||||
|          * End the active drag gesture. This will update the | ||||
|          * view configuration. | ||||
|   | ||||
| @@ -423,6 +423,42 @@ define( | ||||
|                 // Style should have been updated | ||||
|                 expect(controller.selected().style).not.toEqual(oldStyle); | ||||
|             }); | ||||
|  | ||||
|             it("reflects limit status", function () { | ||||
|                 var elements; | ||||
|  | ||||
|                 mockHandle.getDatum.andReturn({}); | ||||
|                 mockHandle.getTelemetryObjects().forEach(function (mockObject) { | ||||
|                     var id = mockObject.getId(), | ||||
|                         mockLimitCapability = | ||||
|                             jasmine.createSpyObj('limit-' + id, ['evaluate']); | ||||
|  | ||||
|                     mockObject.getCapability.andCallFake(function (key) { | ||||
|                         return (key === 'limit') && mockLimitCapability; | ||||
|                     }); | ||||
|  | ||||
|                     mockLimitCapability.evaluate | ||||
|                         .andReturn({ cssClass: 'alarm-' + id }); | ||||
|                 }); | ||||
|  | ||||
|                 // Initialize | ||||
|                 mockScope.domainObject = mockDomainObject; | ||||
|                 mockScope.model = testModel; | ||||
|                 findWatch("domainObject")(mockDomainObject); | ||||
|                 findWatch("model.modified")(1); | ||||
|                 findWatch("model.composition")(mockScope.model.composition); | ||||
|  | ||||
|                 // Invoke the subscription callback | ||||
|                 mockHandler.handle.mostRecentCall.args[1](); | ||||
|  | ||||
|                 // Get elements that controller is now exposing | ||||
|                 elements = controller.getElements(); | ||||
|  | ||||
|                 // Limit-based CSS classes should be available | ||||
|                 expect(elements[0].cssClass).toEqual("alarm-a"); | ||||
|                 expect(elements[1].cssClass).toEqual("alarm-b"); | ||||
|                 expect(elements[2].cssClass).toEqual("alarm-c"); | ||||
|             }); | ||||
|         }); | ||||
|     } | ||||
| ); | ||||
|   | ||||
| @@ -19,7 +19,7 @@ | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
| /*global define,describe,it,expect,beforeEach,jasmine*/ | ||||
| /*global define,describe,it,expect,beforeEach,jasmine,spyOn*/ | ||||
|  | ||||
| define( | ||||
|     ["../src/LayoutController"], | ||||
| @@ -31,21 +31,44 @@ define( | ||||
|                 mockEvent, | ||||
|                 testModel, | ||||
|                 testConfiguration, | ||||
|                 controller; | ||||
|                 controller, | ||||
|                 mockCompositionCapability, | ||||
|                 mockComposition, | ||||
|                 mockCompositionObjects; | ||||
|  | ||||
|             function mockPromise(value){ | ||||
|                 return { | ||||
|                     then: function (thenFunc) { | ||||
|                         return mockPromise(thenFunc(value)); | ||||
|                     } | ||||
|                 }; | ||||
|             } | ||||
|  | ||||
|             function mockDomainObject(id){ | ||||
|                 return { | ||||
|                     getId: function() { | ||||
|                         return id; | ||||
|                     }, | ||||
|                     useCapability: function() { | ||||
|                         return mockCompositionCapability; | ||||
|                     } | ||||
|                 }; | ||||
|             } | ||||
|  | ||||
|             beforeEach(function () { | ||||
|                 mockScope = jasmine.createSpyObj( | ||||
|                     "$scope", | ||||
|                     [ "$watch", "$on", "commit" ] | ||||
|                     [ "$watch", "$watchCollection", "$on", "commit" ] | ||||
|                 ); | ||||
|                 mockEvent = jasmine.createSpyObj( | ||||
|                     'event', | ||||
|                     [ 'preventDefault' ] | ||||
|                 ); | ||||
|  | ||||
|                 testModel = { | ||||
|                     composition: [ "a", "b", "c" ] | ||||
|                 }; | ||||
|                 testModel = {}; | ||||
|  | ||||
|                 mockComposition = ["a", "b", "c"]; | ||||
|                 mockCompositionObjects = mockComposition.map(mockDomainObject); | ||||
|  | ||||
|                 testConfiguration = { | ||||
|                     panels: { | ||||
| @@ -56,23 +79,62 @@ define( | ||||
|                     } | ||||
|                 }; | ||||
|  | ||||
|                 mockCompositionCapability = mockPromise(mockCompositionObjects); | ||||
|  | ||||
|                 mockScope.domainObject = mockDomainObject("mockDomainObject"); | ||||
|                 mockScope.model = testModel; | ||||
|                 mockScope.configuration = testConfiguration; | ||||
|                 spyOn(mockScope.domainObject, "useCapability").andCallThrough(); | ||||
|  | ||||
|                 controller = new LayoutController(mockScope); | ||||
|                 spyOn(controller, "layoutPanels").andCallThrough(); | ||||
|             }); | ||||
|  | ||||
|             // Model changes will indicate that panel positions | ||||
|             // may have changed, for instance. | ||||
|             it("watches for changes to composition", function () { | ||||
|                 expect(mockScope.$watch).toHaveBeenCalledWith( | ||||
|                 expect(mockScope.$watchCollection).toHaveBeenCalledWith( | ||||
|                     "model.composition", | ||||
|                     jasmine.any(Function) | ||||
|                 ); | ||||
|             }); | ||||
|  | ||||
|             it("Retrieves updated composition from composition capability", function () { | ||||
|                 mockScope.$watchCollection.mostRecentCall.args[1](); | ||||
|                 expect(mockScope.domainObject.useCapability).toHaveBeenCalledWith( | ||||
|                     "composition" | ||||
|                 ); | ||||
|                 expect(controller.layoutPanels).toHaveBeenCalledWith( | ||||
|                     mockComposition | ||||
|                 ); | ||||
|             }); | ||||
|  | ||||
|             it("Is robust to concurrent changes to composition", function () { | ||||
|                 var secondMockComposition = ["a", "b", "c", "d"], | ||||
|                     secondMockCompositionObjects = secondMockComposition.map(mockDomainObject), | ||||
|                     firstCompositionCB, | ||||
|                     secondCompositionCB; | ||||
|  | ||||
|                 spyOn(mockCompositionCapability, "then"); | ||||
|                 mockScope.$watchCollection.mostRecentCall.args[1](); | ||||
|                 mockScope.$watchCollection.mostRecentCall.args[1](); | ||||
|  | ||||
|                 firstCompositionCB = mockCompositionCapability.then.calls[0].args[0]; | ||||
|                 secondCompositionCB = mockCompositionCapability.then.calls[1].args[0]; | ||||
|  | ||||
|                 //Resolve promises in reverse order | ||||
|                 secondCompositionCB(secondMockCompositionObjects); | ||||
|                 firstCompositionCB(mockCompositionObjects); | ||||
|  | ||||
|                 //Expect the promise call that was initiated most recently to | ||||
|                 // be the one used to populate scope, irrespective of order that | ||||
|                 // it was eventually resolved | ||||
|                 expect(mockScope.composition).toBe(secondMockCompositionObjects); | ||||
|             }); | ||||
|  | ||||
|  | ||||
|             it("provides styles for frames, from configuration", function () { | ||||
|                 mockScope.$watch.mostRecentCall.args[1](testModel.composition); | ||||
|                 mockScope.$watchCollection.mostRecentCall.args[1](); | ||||
|                 expect(controller.getFrameStyle("a")).toEqual({ | ||||
|                     top: "320px", | ||||
|                     left: "640px", | ||||
| @@ -85,7 +147,7 @@ define( | ||||
|                 var styleB, styleC; | ||||
|  | ||||
|                 // b and c do not have configured positions | ||||
|                 mockScope.$watch.mostRecentCall.args[1](testModel.composition); | ||||
|                 mockScope.$watchCollection.mostRecentCall.args[1](); | ||||
|  | ||||
|                 styleB = controller.getFrameStyle("b"); | ||||
|                 styleC = controller.getFrameStyle("c"); | ||||
| @@ -102,7 +164,7 @@ define( | ||||
|  | ||||
|             it("allows panels to be dragged", function () { | ||||
|                 // Populate scope | ||||
|                 mockScope.$watch.mostRecentCall.args[1](testModel.composition); | ||||
|                 mockScope.$watchCollection.mostRecentCall.args[1](); | ||||
|  | ||||
|                 // Verify precondtion | ||||
|                 expect(testConfiguration.panels.b).not.toBeDefined(); | ||||
| @@ -121,7 +183,7 @@ define( | ||||
|  | ||||
|             it("invokes commit after drag", function () { | ||||
|                 // Populate scope | ||||
|                 mockScope.$watch.mostRecentCall.args[1](testModel.composition); | ||||
|                 mockScope.$watchCollection.mostRecentCall.args[1](); | ||||
|  | ||||
|                 // Do a drag | ||||
|                 controller.startDrag("b", [1, 1], [0, 0]); | ||||
| @@ -147,7 +209,6 @@ define( | ||||
|                 expect(testConfiguration.panels.d).not.toBeDefined(); | ||||
|  | ||||
|                 // Notify that a drop occurred | ||||
|                 testModel.composition.push('d'); | ||||
|                 mockScope.$on.mostRecentCall.args[1]( | ||||
|                     mockEvent, | ||||
|                     'd', | ||||
| @@ -167,7 +228,6 @@ define( | ||||
|                 mockEvent.defaultPrevented = true; | ||||
|  | ||||
|                 // Notify that a drop occurred | ||||
|                 testModel.composition.push('d'); | ||||
|                 mockScope.$on.mostRecentCall.args[1]( | ||||
|                     mockEvent, | ||||
|                     'd', | ||||
| @@ -184,7 +244,7 @@ define( | ||||
|  | ||||
|                 // White-boxy; we know which watch is which | ||||
|                 mockScope.$watch.calls[0].args[1](testModel.layoutGrid); | ||||
|                 mockScope.$watch.calls[1].args[1](testModel.composition); | ||||
|                 mockScope.$watchCollection.calls[0].args[1](testModel.composition); | ||||
|  | ||||
|                 styleB = controller.getFrameStyle("b"); | ||||
|  | ||||
| @@ -201,7 +261,6 @@ define( | ||||
|                 mockScope.$watch.calls[0].args[1](testModel.layoutGrid); | ||||
|  | ||||
|                 // Notify that a drop occurred | ||||
|                 testModel.composition.push('d'); | ||||
|                 mockScope.$on.mostRecentCall.args[1]( | ||||
|                     mockEvent, | ||||
|                     'd', | ||||
| @@ -215,6 +274,23 @@ define( | ||||
|                 expect(parseInt(style.width, 10)).toBeGreaterThan(63); | ||||
|                 expect(parseInt(style.height, 10)).toBeGreaterThan(31); | ||||
|             }); | ||||
|  | ||||
|             it("updates positions of existing objects on a drop", function () { | ||||
|                 var oldStyle; | ||||
|  | ||||
|                 mockScope.$watchCollection.mostRecentCall.args[1](); | ||||
|  | ||||
|                 oldStyle = controller.getFrameStyle("b"); | ||||
|  | ||||
|                 expect(oldStyle).toBeDefined(); | ||||
|  | ||||
|                 // ...drop event... | ||||
|                 mockScope.$on.mostRecentCall | ||||
|                     .args[1](mockEvent, 'b', { x: 300, y: 100 }); | ||||
|  | ||||
|                 expect(controller.getFrameStyle("b")) | ||||
|                     .not.toEqual(oldStyle); | ||||
|             }); | ||||
|         }); | ||||
|     } | ||||
| ); | ||||
|   | ||||
| @@ -146,6 +146,7 @@ define( | ||||
|                     if (canvas.width !== canvas.offsetWidth || | ||||
|                             canvas.height !== canvas.offsetHeight) { | ||||
|                         doDraw(scope.draw); | ||||
|                         scope.$apply(); | ||||
|                     } | ||||
|                 } | ||||
|  | ||||
| @@ -181,7 +182,7 @@ define( | ||||
|                 canvas.addEventListener("webglcontextlost", fallbackFromWebGL); | ||||
|  | ||||
|                 // Check for resize, on a timer | ||||
|                 activeInterval = $interval(drawIfResized, 1000); | ||||
|                 activeInterval = $interval(drawIfResized, 1000, 0, false); | ||||
|  | ||||
|                 // Watch "draw" for external changes to the set of | ||||
|                 // things to be drawn. | ||||
|   | ||||
| @@ -45,8 +45,10 @@ define( | ||||
|                     jasmine.createSpy("$interval"); | ||||
|                 mockLog = | ||||
|                     jasmine.createSpyObj("$log", ["warn", "info", "debug"]); | ||||
|                 mockScope = | ||||
|                     jasmine.createSpyObj("$scope", ["$watchCollection", "$on"]); | ||||
|                 mockScope = jasmine.createSpyObj( | ||||
|                     "$scope", | ||||
|                     ["$watchCollection", "$on", "$apply"] | ||||
|                 ); | ||||
|                 mockElement = | ||||
|                     jasmine.createSpyObj("element", ["find", "html"]); | ||||
|                 mockInterval.cancel = jasmine.createSpy("cancelInterval"); | ||||
| @@ -152,7 +154,9 @@ define( | ||||
|                 // Should track canvas size in an interval | ||||
|                 expect(mockInterval).toHaveBeenCalledWith( | ||||
|                     jasmine.any(Function), | ||||
|                     jasmine.any(Number) | ||||
|                     jasmine.any(Number), | ||||
|                     0, | ||||
|                     false | ||||
|                 ); | ||||
|  | ||||
|                 // Verify pre-condition | ||||
|   | ||||
| @@ -285,6 +285,33 @@ define( | ||||
|                 fireWatch("axes[1].active.key", 'someNewKey'); | ||||
|                 expect(mockHandle.request.calls.length).toEqual(2); | ||||
|             }); | ||||
|  | ||||
|             it("provides classes for legends based on limit state", function () { | ||||
|                 var mockTelemetryObjects = mockHandle.getTelemetryObjects(); | ||||
|  | ||||
|                 mockHandle.getDatum.andReturn({}); | ||||
|                 mockTelemetryObjects.forEach(function (mockObject, i) { | ||||
|                     var id = 'object-' + i, | ||||
|                         mockLimitCapability = | ||||
|                             jasmine.createSpyObj('limit-' + id, ['evaluate']); | ||||
|  | ||||
|                     mockObject.getId.andReturn(id); | ||||
|                     mockObject.getCapability.andCallFake(function (key) { | ||||
|                         return (key === 'limit') && mockLimitCapability; | ||||
|                     }); | ||||
|  | ||||
|                     mockLimitCapability.evaluate | ||||
|                         .andReturn({ cssClass: 'alarm-' + id }); | ||||
|                 }); | ||||
|  | ||||
|                 mockScope.$watch.mostRecentCall.args[1](mockDomainObject); | ||||
|                 mockHandler.handle.mostRecentCall.args[1](); | ||||
|  | ||||
|                 mockTelemetryObjects.forEach(function (mockTelemetryObject) { | ||||
|                     expect(controller.getLegendClass(mockTelemetryObject)) | ||||
|                         .toEqual('alarm-' + mockTelemetryObject.getId()); | ||||
|                 }); | ||||
|             }); | ||||
|         }); | ||||
|     } | ||||
| ); | ||||
|   | ||||
							
								
								
									
										103
									
								
								platform/features/plot/test/elements/PlotLimitTrackerSpec.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										103
									
								
								platform/features/plot/test/elements/PlotLimitTrackerSpec.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,103 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT Web, Copyright (c) 2014-2015, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
|  * Administration. All rights reserved. | ||||
|  * | ||||
|  * Open MCT Web 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 Web 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. | ||||
|  *****************************************************************************/ | ||||
| /*global define,Promise,describe,it,expect,beforeEach,waitsFor,jasmine*/ | ||||
|  | ||||
| define( | ||||
|     ["../../src/elements/PlotLimitTracker"], | ||||
|     function (PlotLimitTracker) { | ||||
|         "use strict"; | ||||
|  | ||||
|         describe("A plot's limit tracker", function () { | ||||
|             var mockHandle, | ||||
|                 testRange, | ||||
|                 mockTelemetryObjects, | ||||
|                 testData, | ||||
|                 mockLimitCapabilities, | ||||
|                 tracker; | ||||
|  | ||||
|             beforeEach(function () { | ||||
|                 testRange = "some-range"; | ||||
|                 testData = {}; | ||||
|                 mockHandle = jasmine.createSpyObj( | ||||
|                     'handle', | ||||
|                     ['getTelemetryObjects', 'getDatum'] | ||||
|                 ); | ||||
|                 mockTelemetryObjects = ['a', 'b', 'c'].map(function (id, i) { | ||||
|                     var mockTelemetryObject = jasmine.createSpyObj( | ||||
|                             'object-' + id, | ||||
|                             [ 'getId', 'getCapability', 'getModel' ] | ||||
|                         ), | ||||
|                         mockLimitCapability = jasmine.createSpyObj( | ||||
|                             'limit-' + id, | ||||
|                             [ 'evaluate' ] | ||||
|                         ); | ||||
|                     testData[id] = { id: id, value: i }; | ||||
|                     mockTelemetryObject.getId.andReturn(id); | ||||
|                     mockTelemetryObject.getCapability.andCallFake(function (key) { | ||||
|                         return key === 'limit' && mockLimitCapability; | ||||
|                     }); | ||||
|                     mockLimitCapability.evaluate | ||||
|                         .andReturn({ cssClass: 'alarm-' + id}); | ||||
|                     return mockTelemetryObject; | ||||
|                 }); | ||||
|                 mockHandle.getTelemetryObjects.andReturn(mockTelemetryObjects); | ||||
|                 mockHandle.getDatum.andCallFake(function (telemetryObject) { | ||||
|                     return testData[telemetryObject.getId()]; | ||||
|                 }); | ||||
|  | ||||
|                 tracker = new PlotLimitTracker(mockHandle, testRange); | ||||
|             }); | ||||
|  | ||||
|             it("initially provides no limit state", function () { | ||||
|                 mockTelemetryObjects.forEach(function (mockTelemetryObject) { | ||||
|                     expect(tracker.getLegendClass(mockTelemetryObject)) | ||||
|                         .toBeUndefined(); | ||||
|                 }); | ||||
|             }); | ||||
|  | ||||
|             describe("when asked to update", function () { | ||||
|                 beforeEach(function () { | ||||
|                     tracker.update(); | ||||
|                 }); | ||||
|  | ||||
|                 it("evaluates limits using the limit capability", function () { | ||||
|                     mockTelemetryObjects.forEach(function (mockTelemetryObject) { | ||||
|                         var id = mockTelemetryObject.getId(), | ||||
|                             mockLimit = | ||||
|                                 mockTelemetryObject.getCapability('limit'); | ||||
|                         expect(mockLimit.evaluate) | ||||
|                             .toHaveBeenCalledWith(testData[id], testRange); | ||||
|                     }); | ||||
|                 }); | ||||
|  | ||||
|                 it("exposes legend classes returned by the limit capability", function () { | ||||
|                     mockTelemetryObjects.forEach(function (mockTelemetryObject) { | ||||
|                         var id = mockTelemetryObject.getId(); | ||||
|                         expect(tracker.getLegendClass(mockTelemetryObject)) | ||||
|                             .toEqual('alarm-' + id); | ||||
|                     }); | ||||
|                 }); | ||||
|             }); | ||||
|  | ||||
|         }); | ||||
|     } | ||||
| ); | ||||
| @@ -6,6 +6,7 @@ | ||||
|     "SubPlot", | ||||
|     "SubPlotFactory", | ||||
|     "elements/PlotAxis", | ||||
|     "elements/PlotLimitTracker", | ||||
|     "elements/PlotLine", | ||||
|     "elements/PlotLineBuffer", | ||||
|     "elements/PlotPalette", | ||||
|   | ||||
| @@ -54,6 +54,13 @@ define( | ||||
|                     // Pass the template URL to ng-include via scope. | ||||
|                     scope.inclusion = controlMap[key]; | ||||
|                 }); | ||||
|                 scope.$watch("structure.validate", function (validate) { | ||||
|                     if (typeof validate === 'function') { | ||||
|                         ngModelController.$validators.custom = validate; | ||||
|                     } else { | ||||
|                         delete ngModelController.$validators.custom; | ||||
|                     } | ||||
|                 }); | ||||
|                 scope.ngModelController = ngModelController; | ||||
|             } | ||||
|  | ||||
| @@ -79,6 +86,9 @@ define( | ||||
|                     // Used to choose which form control to use | ||||
|                     key: "=", | ||||
|  | ||||
|                     // Allow controls to trigger blur-like events | ||||
|                     ngBlur: "&", | ||||
|  | ||||
|                     // The state of the form value itself | ||||
|                     ngModel: "=", | ||||
|  | ||||
|   | ||||
| @@ -80,7 +80,7 @@ define( | ||||
|  | ||||
|             // Update the indicator initially, and start polling. | ||||
|             updateIndicator(); | ||||
|             $interval(updateIndicator, interval, false); | ||||
|             $interval(updateIndicator, interval, 0, false); | ||||
|         } | ||||
|  | ||||
|         ElasticIndicator.prototype.getGlyph = function () { | ||||
|   | ||||
| @@ -55,6 +55,7 @@ define( | ||||
|                 expect(mockInterval).toHaveBeenCalledWith( | ||||
|                     jasmine.any(Function), | ||||
|                     testInterval, | ||||
|                     0, | ||||
|                     false | ||||
|                 ); | ||||
|             }); | ||||
|   | ||||
| @@ -68,6 +68,20 @@ | ||||
|                     "agentService" | ||||
|                 ] | ||||
|             } | ||||
|         ], | ||||
|         "runs": [ | ||||
|             { | ||||
|                 "priority": "mandatory", | ||||
|                 "implementation": "TemplatePrefetcher.js", | ||||
|                 "depends": [ | ||||
|                     "templateLinker", | ||||
|                     "templates[]", | ||||
|                     "views[]", | ||||
|                     "representations[]", | ||||
|                     "controls[]", | ||||
|                     "containers[]" | ||||
|                 ] | ||||
|             } | ||||
|         ] | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -96,7 +96,7 @@ define( | ||||
|                     toClear = [], // Properties to clear out of scope on change | ||||
|                     counter = 0, | ||||
|                     couldRepresent = false, | ||||
|                     lastId, | ||||
|                     lastIdPath = [], | ||||
|                     lastKey, | ||||
|                     changeTemplate = templateLinker.link($scope, element); | ||||
|  | ||||
| @@ -143,11 +143,27 @@ define( | ||||
|                     }); | ||||
|                 } | ||||
|  | ||||
|                 function unchanged(canRepresent, id, key) { | ||||
|                 function unchanged(canRepresent, idPath, key) { | ||||
|                     return canRepresent && | ||||
|                         couldRepresent && | ||||
|                         id === lastId && | ||||
|                         key === lastKey; | ||||
|                         key === lastKey && | ||||
|                         idPath.length === lastIdPath.length && | ||||
|                         idPath.every(function (id, i) { | ||||
|                             return id === lastIdPath[i]; | ||||
|                         }); | ||||
|                 } | ||||
|  | ||||
|                 function getIdPath(domainObject) { | ||||
|                     if (!domainObject) { | ||||
|                         return []; | ||||
|                     } | ||||
|                     if (!domainObject.hasCapability('context')) { | ||||
|                         return [domainObject.getId()]; | ||||
|                     } | ||||
|                     return domainObject.getCapability('context') | ||||
|                         .getPath().map(function (pathObject) { | ||||
|                             return pathObject.getId(); | ||||
|                         }); | ||||
|                 } | ||||
|  | ||||
|                 // General-purpose refresh mechanism; should set up the scope | ||||
| @@ -159,10 +175,10 @@ define( | ||||
|                         path = representation && getPath(representation), | ||||
|                         uses = ((representation || {}).uses || []), | ||||
|                         canRepresent = !!(path && domainObject), | ||||
|                         id = domainObject && domainObject.getId(), | ||||
|                         idPath = getIdPath(domainObject), | ||||
|                         key = $scope.key; | ||||
|  | ||||
|                     if (unchanged(canRepresent, id, key)) { | ||||
|                     if (unchanged(canRepresent, idPath, key)) { | ||||
|                         return; | ||||
|                     } | ||||
|  | ||||
| @@ -190,7 +206,7 @@ define( | ||||
|  | ||||
|                     // To allow simplified change detection next time around | ||||
|                     couldRepresent = canRepresent; | ||||
|                     lastId = id; | ||||
|                     lastIdPath = idPath; | ||||
|                     lastKey = key; | ||||
|  | ||||
|                     // Populate scope with fields associated with the current | ||||
|   | ||||
| @@ -54,7 +54,6 @@ define( | ||||
|          * @param {string} the URL for the template | ||||
|          * @returns {Promise.<string>} a promise for the HTML content of | ||||
|          *          the template | ||||
|          * @private | ||||
|          */ | ||||
|         TemplateLinker.prototype.load = function (templateUrl) { | ||||
|             return this.$templateRequest( | ||||
|   | ||||
							
								
								
									
										51
									
								
								platform/representation/src/TemplatePrefetcher.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								platform/representation/src/TemplatePrefetcher.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,51 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT Web, Copyright (c) 2014-2015, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
|  * Administration. All rights reserved. | ||||
|  * | ||||
|  * Open MCT Web 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 Web 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. | ||||
|  *****************************************************************************/ | ||||
| /*global define,Promise*/ | ||||
|  | ||||
| define( | ||||
|     function () { | ||||
|         'use strict'; | ||||
|  | ||||
|         /** | ||||
|          * Loads all templates when the application is started. | ||||
|          * @param {platform/representation.TemplateLinker} templateLinker | ||||
|          *        the `templateLinker` service, used to load and cache | ||||
|          *        template extensions | ||||
|          * @param {...Array.<{templateUrl: string}>} extensions arrays | ||||
|          *        of template or template-like extensions | ||||
|          */ | ||||
|         function TemplatePrefetcher(templateLinker, extensions) { | ||||
|             Array.prototype.slice.apply(arguments, [1]) | ||||
|                 .reduce(function (a, b) { | ||||
|                     return a.concat(b); | ||||
|                 }, []) | ||||
|                 .map(function (ext) { | ||||
|                     return templateLinker.getPath(ext); | ||||
|                 }) | ||||
|                 .forEach(function (path) { | ||||
|                     templateLinker.load(path); | ||||
|                 }); | ||||
|         } | ||||
|  | ||||
|         return TemplatePrefetcher; | ||||
|     } | ||||
| ); | ||||
| @@ -247,6 +247,54 @@ define( | ||||
|                 mockScope.$watch.calls[0].args[1](); | ||||
|                 expect(mockScope.testCapability).toBeUndefined(); | ||||
|             }); | ||||
|  | ||||
|             it("detects changes among linked instances", function () { | ||||
|                 var mockContext = jasmine.createSpyObj('context', ['getPath']), | ||||
|                     mockContext2 = jasmine.createSpyObj('context', ['getPath']), | ||||
|                     mockLink = jasmine.createSpyObj( | ||||
|                         'linkedObject', | ||||
|                         DOMAIN_OBJECT_METHODS | ||||
|                     ), | ||||
|                     mockParent = jasmine.createSpyObj( | ||||
|                         'parentObject', | ||||
|                         DOMAIN_OBJECT_METHODS | ||||
|                     ), | ||||
|                     callCount; | ||||
|  | ||||
|                 mockDomainObject.getCapability.andCallFake(function (c) { | ||||
|                     return c === 'context' && mockContext; | ||||
|                 }); | ||||
|                 mockLink.getCapability.andCallFake(function (c) { | ||||
|                     return c === 'context' && mockContext2; | ||||
|                 }); | ||||
|                 mockDomainObject.hasCapability.andCallFake(function (c) { | ||||
|                     return c === 'context'; | ||||
|                 }); | ||||
|                 mockLink.hasCapability.andCallFake(function (c) { | ||||
|                     return c === 'context'; | ||||
|                 }); | ||||
|                 mockLink.getModel.andReturn({}); | ||||
|  | ||||
|                 mockContext.getPath.andReturn([mockDomainObject]); | ||||
|                 mockContext2.getPath.andReturn([mockParent, mockLink]); | ||||
|  | ||||
|                 mockLink.getId.andReturn('test-id'); | ||||
|                 mockDomainObject.getId.andReturn('test-id'); | ||||
|  | ||||
|                 mockParent.getId.andReturn('parent-id'); | ||||
|  | ||||
|                 mockScope.key = "abc"; | ||||
|                 mockScope.domainObject = mockDomainObject; | ||||
|  | ||||
|                 mockScope.$watch.calls[0].args[1](); | ||||
|                 callCount = mockChangeTemplate.calls.length; | ||||
|  | ||||
|                 mockScope.domainObject = mockLink; | ||||
|                 mockScope.$watch.calls[0].args[1](); | ||||
|  | ||||
|                 expect(mockChangeTemplate.calls.length) | ||||
|                     .toEqual(callCount + 1); | ||||
|             }); | ||||
|         }); | ||||
|     } | ||||
| ); | ||||
|   | ||||
							
								
								
									
										76
									
								
								platform/representation/test/TemplatePrefetcherSpec.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										76
									
								
								platform/representation/test/TemplatePrefetcherSpec.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,76 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT Web, Copyright (c) 2014-2015, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
|  * Administration. All rights reserved. | ||||
|  * | ||||
|  * Open MCT Web 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 Web 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. | ||||
|  *****************************************************************************/ | ||||
| /*global define,Promise,describe,it,expect,beforeEach,waitsFor,jasmine*/ | ||||
|  | ||||
|  | ||||
| define( | ||||
|     ["../src/TemplatePrefetcher"], | ||||
|     function (TemplatePrefetcher) { | ||||
|         'use strict'; | ||||
|  | ||||
|         describe("TemplatePrefetcher", function () { | ||||
|             var mockTemplateLinker, | ||||
|                 testExtensions, | ||||
|                 testPathPrefix, | ||||
|                 prefetcher; | ||||
|  | ||||
|             beforeEach(function () { | ||||
|                 testPathPrefix = "some/path/"; | ||||
|  | ||||
|                 mockTemplateLinker = jasmine.createSpyObj( | ||||
|                     'templateLinker', | ||||
|                     [ 'getPath', 'load' ] | ||||
|                 ); | ||||
|  | ||||
|                 mockTemplateLinker.getPath.andCallFake(function (ext) { | ||||
|                     return testPathPrefix + ext.templateUrl; | ||||
|                 }); | ||||
|  | ||||
|                 testExtensions = ['a', 'b', 'c'].map(function (category) { | ||||
|                     return ['x', 'y', 'z'].map(function (ext) { | ||||
|                         return { | ||||
|                             templateUrl: category + '/' + ext + '.html' | ||||
|                         }; | ||||
|                     }); | ||||
|                 }); | ||||
|  | ||||
|                 prefetcher = new TemplatePrefetcher( | ||||
|                     mockTemplateLinker, | ||||
|                     testExtensions[0], | ||||
|                     testExtensions[1], | ||||
|                     testExtensions[2] | ||||
|                 ); | ||||
|             }); | ||||
|  | ||||
|             it("loads all templates when run", function () { | ||||
|                 testExtensions.forEach(function (category) { | ||||
|                     category.forEach(function (extension) { | ||||
|                         expect(mockTemplateLinker.load).toHaveBeenCalledWith( | ||||
|                             mockTemplateLinker.getPath(extension) | ||||
|                         ); | ||||
|                     }); | ||||
|                 }); | ||||
|             }); | ||||
|  | ||||
|         }); | ||||
|     } | ||||
| ); | ||||
| @@ -8,5 +8,6 @@ | ||||
|     "services/DndService", | ||||
|     "MCTInclude", | ||||
|     "MCTRepresentation", | ||||
|     "TemplateLinker" | ||||
|     "TemplateLinker", | ||||
|     "TemplatePrefetcher" | ||||
| ] | ||||
|   | ||||
| @@ -20,11 +20,12 @@ | ||||
|  at runtime from the About dialog for additional information. | ||||
| -->  | ||||
|  | ||||
| <div class="search-result-item" | ||||
| <div class="search-result-item l-flex-row flex-elem grows" | ||||
|      ng-class="{selected: ngModel.selectedObject.getId() === domainObject.getId()}"> | ||||
|     <mct-representation key="'label'" | ||||
|                         mct-object="domainObject" | ||||
|                         ng-model="ngModel" | ||||
|                         ng-click="ngModel.selectedObject = domainObject"> | ||||
|                         ng-click="ngModel.selectedObject = domainObject" | ||||
|                         class="l-flex-row flex-elem grows"> | ||||
|     </mct-representation> | ||||
| </div> | ||||
| @@ -20,11 +20,13 @@ | ||||
|  at runtime from the About dialog for additional information. | ||||
| --> | ||||
| <div class="l-flex-col flex-elem grows holder holder-search"  ng-controller="SearchController as controller"> | ||||
|     <div class="search-bar flex-elem" ng-controller="ClickAwayController as toggle"> | ||||
|     <div class="search-bar flex-elem" | ||||
|          ng-controller="ClickAwayController as toggle" | ||||
|          ng-class="{ holder: !(ngModel.input === '' || ngModel.input === undefined) }"> | ||||
|         <input class="search-input" | ||||
|                type="text" | ||||
|                ng-model="ngModel.input" | ||||
|                ng-keyup="controller.search()" /> | ||||
|                ng-keyup="controller.search()"/> | ||||
|         <a class="clear-icon" | ||||
|            ng-class="{show: !(ngModel.input === '' || ngModel.input === undefined)}" | ||||
|            ng-click="ngModel.input = ''; controller.search()"></a> | ||||
| @@ -37,18 +39,19 @@ | ||||
|                      ng-click="toggle.setState(true)"> | ||||
|         </mct-include> | ||||
|     </div> | ||||
|     <div class="active-filter-display flex-elem" | ||||
|     <div class="active-filter-display flex-elem holder" | ||||
|          ng-class="{off: ngModel.filtersString === '' || ngModel.filtersString === undefined || !ngModel.search}" | ||||
|          ng-controller="SearchMenuController as menuController"> | ||||
|         <a class="clear-icon clear-filters-icon" | ||||
|            ng-click="ngModel.checkAll = true; menuController.checkAll()"></a>Filtered by: {{ ngModel.filtersString }} | ||||
|     </div> | ||||
|     <div class="search-results flex-elem grows vscroll" | ||||
|     <div class="search-results flex-elem holder grows vscroll" | ||||
|          ng-class="{ off: !(loading || results.length > 0), loading: loading }"> | ||||
|         <mct-representation key="'search-item'" | ||||
|                             ng-repeat="result in results" | ||||
|                             mct-object="result.object" | ||||
|                             ng-model="ngModel"> | ||||
|                             ng-model="ngModel" | ||||
|                             class="l-flex-row flex-elem grows"> | ||||
|         </mct-representation> | ||||
|         <a class="load-more-button s-btn vsm" ng-if="controller.areMore()" ng-click="controller.loadMore()">More Results</a> | ||||
|     </div> | ||||
|   | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user