├── .gitignore ├── Gruntfile.js ├── LICENSE ├── README.md ├── package.json ├── pom.xml ├── screenshot_portlet.png ├── screenshot_standalone.png └── src └── main ├── java └── at │ └── nonblocking │ └── portlet │ └── angularjs │ ├── controller │ └── PortletController.java │ ├── model │ ├── User.java │ ├── UserDetail.java │ ├── UserList.java │ └── UserListRequest.java │ └── service │ ├── UserService.java │ └── UserServiceImpl.java ├── javascript ├── app.js ├── controllers │ ├── main_controller.js │ ├── user_detail_controller.js │ └── user_list_controller.js ├── directives │ └── router_view.js └── services │ ├── backend.js │ └── router.js ├── resources └── logback.xml ├── sass └── app.scss └── webapp ├── WEB-INF ├── liferay-display.xml ├── liferay-plugin-package.properties ├── liferay-portlet.xml ├── portlet.xml ├── spring │ ├── portlet-context.xml │ └── root-context.xml └── web.xml ├── index.html ├── templates ├── userDetail.html └── userList.html └── testdata ├── userDetails.json ├── users.json └── users2.json /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | .idea 3 | *.iml 4 | .classpath 5 | .project 6 | .settings/* 7 | sass-cache 8 | bower_components 9 | node_modules 10 | target 11 | *.log 12 | hs_err_pid* 13 | 14 | #Generated resources 15 | src/main/webapp/css/* 16 | src/main/webapp/js/* 17 | 18 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | 2 | var jsApp = [ 3 | 'src/main/javascript/app.js', 4 | 'src/main/javascript/controllers/main_controller.js', 5 | 'src/main/javascript/controllers/user_list_controller.js', 6 | 'src/main/javascript/controllers/user_detail_controller.js', 7 | 'src/main/javascript/directives/router_view.js', 8 | 'src/main/javascript/services/router.js', 9 | 'src/main/javascript/services/backend.js' 10 | ]; 11 | 12 | var jsVendor = [ 13 | 'node_modules/angular/angular.min.js' 14 | ]; 15 | 16 | module.exports = function(grunt) { 17 | 18 | grunt 19 | .initConfig({ 20 | pkg : grunt.file.readJSON('package.json'), 21 | 22 | jshint : { 23 | src : [ 'Gruntfile.js', 'src/main/javascript/*.js' ] 24 | }, 25 | 26 | sass: { 27 | dev: { 28 | options: { 29 | style: 'expanded' 30 | }, 31 | files: { 32 | 'src/main/webapp/css/app.css': 'src/main/sass/app.scss' 33 | } 34 | }, 35 | dist: { 36 | options: { 37 | style: 'compressed' 38 | }, 39 | files: { 40 | 'src/main/webapp/css/app.css': 'src/main/sass/app.scss' 41 | } 42 | } 43 | }, 44 | 45 | concat : { 46 | jsVendor : { 47 | src : jsVendor, 48 | dest : 'src/main/webapp/js/vendor.js' 49 | }, 50 | jsApp : { 51 | src : jsApp, 52 | dest : 'src/main/webapp/js/app.js' 53 | } 54 | }, 55 | 56 | ngAnnotate: { 57 | jsApp : { 58 | files: { 59 | 'src/main/webapp/js/app.js': 'src/main/webapp/js/app.js' 60 | } 61 | } 62 | }, 63 | 64 | uglify: { 65 | jsApp : { 66 | files: { 67 | 'src/main/webapp/js/app.js': 'src/main/webapp/js/app.js' 68 | } 69 | } 70 | }, 71 | 72 | connect : { 73 | options : { 74 | port : 9000, 75 | hostname : 'localhost' 76 | }, 77 | livereload : { 78 | options : { 79 | open : true, 80 | base : [ 'src/main/webapp/' ] 81 | } 82 | } 83 | }, 84 | 85 | watch : { 86 | options : { 87 | livereload : true 88 | }, 89 | html : { 90 | files : [ 'src/main/webapp/*.html', 'src/main/webapp/templates/*.html' ] 91 | }, 92 | css : { 93 | files : [ 'src/main/sass/**/*.scss' ], 94 | tasks : [ 'sass:dev' ] 95 | }, 96 | js : { 97 | files : [ 'src/main/javascript/**/*.js' ], 98 | tasks : [ 'jshint', 'concat:jsApp' ] 99 | } 100 | } 101 | 102 | }); 103 | 104 | grunt.loadNpmTasks('grunt-contrib-jshint'); 105 | grunt.loadNpmTasks('grunt-contrib-sass'); 106 | grunt.loadNpmTasks('grunt-contrib-watch'); 107 | grunt.loadNpmTasks('grunt-contrib-connect'); 108 | grunt.loadNpmTasks('grunt-contrib-uglify'); 109 | grunt.loadNpmTasks('grunt-contrib-concat'); 110 | grunt.loadNpmTasks('grunt-ng-annotate'); 111 | 112 | grunt.registerTask('server', function(target) { 113 | grunt.task.run([ 'jshint', 'sass:dev', 'concat:jsVendor', 'concat:jsApp', 'connect:livereload', 'watch']); 114 | }); 115 | 116 | // Default task 117 | grunt.registerTask('default', [ 'jshint', 'sass:dev', 'concat:jsVendor', 'concat:jsApp' ]); 118 | 119 | //Distribution task 120 | grunt.registerTask('dist', [ 'jshint', 'sass:dist', 'concat:jsVendor', 'concat:jsApp', 'ngAnnotate:jsApp', 'uglify:jsApp' ]); 121 | }; 122 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | AngularJS Portlet Demo v3 2 | ========================= 3 | 4 | A Liferay portlet that just list all users and shows how AngularJS and Spring Portlet MVC can be used to create single page HTML5 portlets. 5 | 6 | The demonstrator consists of two parts: 7 | 8 | 1. A [Spring Portlet MVC](http://docs.spring.io/spring/docs/4.0.x/spring-framework-reference/html/portlet.html) backend that handles all AJAX requests as resource requests. 9 | It leverages the [thymeleaf](http://www.thymeleaf.org/) HTML5 template engine to pass the resource URL and other parameters as JavaScript variables to the frontend. 10 | 2. An [AngularJS](https://angularjs.org/) frontend that can also be run **standalone with node.js**, which can greatly accelerate the development. 11 | 12 | ## Build 13 | 14 | ### Prerequisites 15 | * [Maven](https://maven.apache.org/) build tool (should come with your favorite IDE) 16 | * [node.js](http://nodejs.org/) and [Grunt](http://gruntjs.com/) to build and run the HTML5/JavaScript code 17 | * [SASS](http://sass-lang.com/) to compile the Sassy CSS into CSS 18 | 19 | ### Create the WAR file 20 | 21 | Just run **mvn package** on the command line. 22 | 23 | ## Run 24 | 25 | ### On a Liferay 6.2 Portal 26 | 27 | Drop the generated _liferay-angularjs-portlet-*.war_ file in the_target_ directory into the *{liferay-home}/deploy* folder. 28 | Alternatively execute on the command line: **mvn liferay:deploy -D\** 29 | 30 | ### Standalone 31 | 32 | Run **npm install** and **grunt server** in the root directory of the project. The webapp will be available on *localhost:9000*. 33 | 34 | ## Notes 35 | 36 | * To run the portlet on a Liferay 6.1 portal two files need to be changed: 37 | 1. In *liferay-portlet.xml* change the doctype from *6.2.0.dtd* to *6.0.0.dtd* and remove the line *<requires-namespaced-parameters>false</requires-namespaced-parameters>* 38 | 2. In *liferay-plugin-package.properties* change the *liferay-versions* property to *6.1.0+,6.1.2+* 39 | * The AngularJS portlet in this demonstrator is **instanceable**, which means that you can place it multiple times on a single portal page 40 | * In order to allow multiple angular based portlets on a page *angular-route* cannot be used 41 | * If you plan to make a whole suite of AngularJS portlets you should put all JavaScript base libraries and base modules into your theme 42 | (instead of shipping it with every single portlet). 43 | * In the standalone version live-reload is activated, so whenever you change a SASS or JS file the browser will refresh automatically. 44 | * In a real world app **don't forget Java and JavaScript unit tests**! 45 | 46 | ## Screenshots 47 | 48 | ### Liferay 6.2 49 | 50 | ![Portlet](screenshot_portlet.png) 51 | 52 | ### Standalone 53 | 54 | ![Portlet](screenshot_standalone.png) 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "liferay-angularjs-portlet", 3 | "version": "3.0.0", 4 | "dependencies": { 5 | "angular": "1.4.10" 6 | }, 7 | "devDependencies": { 8 | "grunt": "1.0.1", 9 | "grunt-contrib-watch": "1.0.0", 10 | "grunt-contrib-jshint": "1.0.0", 11 | "grunt-contrib-sass": "1.0.0", 12 | "grunt-contrib-connect": "1.0.2", 13 | "grunt-contrib-uglify": "1.0.1", 14 | "grunt-contrib-concat": "1.0.1", 15 | "grunt-ng-annotate": "2.0.2" 16 | }, 17 | "scripts": { 18 | "dist": "grunt dist", 19 | "run": "grunt server" 20 | } 21 | } -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 4.0.0 7 | at.nonblocking 8 | liferay-angularjs-portlet 9 | 3.0.0 10 | war 11 | AngularJS 1 Liferay Demo Portlet 12 | 13 | 14 | 6.2.1 15 | 4.2.1.RELEASE 16 | 17 | 18 | 19 | 20 | javax.inject 21 | javax.inject 22 | 1 23 | 24 | 25 | javax.portlet 26 | portlet-api 27 | 2.0 28 | provided 29 | 30 | 31 | com.liferay.portal 32 | portal-service 33 | ${liferay.version} 34 | provided 35 | 36 | 37 | 38 | org.springframework 39 | spring-webmvc-portlet 40 | ${spring.version} 41 | 42 | 43 | org.thymeleaf 44 | thymeleaf-spring4 45 | 2.1.3.RELEASE 46 | 47 | 48 | com.fasterxml.jackson.core 49 | jackson-databind 50 | 2.2.3 51 | 52 | 53 | 54 | org.apache.commons 55 | commons-lang3 56 | 3.3.2 57 | 58 | 59 | ch.qos.logback 60 | logback-classic 61 | 1.1.3 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | maven-compiler-plugin 70 | 2.5 71 | 72 | 1.8 73 | 1.8 74 | 75 | 76 | 77 | 78 | com.github.eirslett 79 | frontend-maven-plugin 80 | 0.0.27 81 | 82 | v5.4.0 83 | 3.5.3 84 | http://nodejs.org/dist/ 85 | http://registry.npmjs.org/npm/-/ 86 | target 87 | 88 | 89 | 90 | install node and npm 91 | 92 | install-node-and-npm 93 | 94 | generate-resources 95 | 96 | 97 | npm install 98 | 99 | npm 100 | 101 | generate-resources 102 | 103 | install 104 | 105 | 106 | 107 | grunt dist 108 | 109 | grunt 110 | 111 | process-resources 112 | 113 | dist 114 | 115 | 116 | 117 | 118 | 119 | 120 | com.liferay.maven.plugins 121 | liferay-maven-plugin 122 | 123 | ${liferay.base.dir}/deploy 124 | ${liferay.version} 125 | portlet 126 | 127 | 128 | 129 | 130 | 131 | -------------------------------------------------------------------------------- /screenshot_portlet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nonblocking/liferay-angularjs-portlet/1fc01c525f5a7f0d715546c4945012ef4d5e9a7e/screenshot_portlet.png -------------------------------------------------------------------------------- /screenshot_standalone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nonblocking/liferay-angularjs-portlet/1fc01c525f5a7f0d715546c4945012ef4d5e9a7e/screenshot_standalone.png -------------------------------------------------------------------------------- /src/main/java/at/nonblocking/portlet/angularjs/controller/PortletController.java: -------------------------------------------------------------------------------- 1 | package at.nonblocking.portlet.angularjs.controller; 2 | 3 | import at.nonblocking.portlet.angularjs.model.UserDetail; 4 | import at.nonblocking.portlet.angularjs.model.UserList; 5 | import at.nonblocking.portlet.angularjs.service.UserService; 6 | import com.fasterxml.jackson.databind.ObjectMapper; 7 | import com.liferay.portal.kernel.util.WebKeys; 8 | import com.liferay.portal.model.User; 9 | import com.liferay.portal.theme.PortletDisplay; 10 | import com.liferay.portal.theme.ThemeDisplay; 11 | import org.slf4j.Logger; 12 | import org.slf4j.LoggerFactory; 13 | import org.springframework.stereotype.Component; 14 | import org.springframework.ui.ModelMap; 15 | import org.springframework.web.bind.annotation.RequestMapping; 16 | import org.springframework.web.bind.annotation.RequestParam; 17 | import org.springframework.web.portlet.bind.annotation.RenderMapping; 18 | import org.springframework.web.portlet.bind.annotation.ResourceMapping; 19 | 20 | import javax.inject.Inject; 21 | import javax.portlet.*; 22 | 23 | @Component 24 | @RequestMapping("view") 25 | public class PortletController { 26 | 27 | private static final Logger LOG = LoggerFactory.getLogger(PortletController.class); 28 | 29 | private static final ObjectMapper JSON_MAPPER = new ObjectMapper(); 30 | 31 | @Inject 32 | private UserService userService; 33 | 34 | @RenderMapping 35 | public String view(RenderRequest request, RenderResponse response, ModelMap model) { 36 | User user = (User) request.getAttribute(WebKeys.USER); 37 | String userScreenName = user != null ? user.getScreenName() : "anonymous"; 38 | 39 | ResourceURL baseResourceUrl = response.createResourceURL(); 40 | 41 | model.addAttribute("ajaxURL", baseResourceUrl.toString()); 42 | model.addAttribute("standalone", false); 43 | model.addAttribute("authenticatedUser", userScreenName); 44 | model.addAttribute("portletId", getPortletId(request)); 45 | model.addAttribute("portletAppContextPath", request.getContextPath() + "/"); 46 | 47 | return "index"; 48 | } 49 | 50 | @ResourceMapping("userList") 51 | public void userList(@RequestParam int startIndex, @RequestParam int limit, ResourceResponse response) throws Exception { 52 | LOG.debug("Got list request for users with startIndex {} and limit {}", startIndex, limit); 53 | 54 | UserList users = this.userService.getPortalUserList(startIndex, limit); 55 | 56 | response.setContentType("application/json"); 57 | response.setCharacterEncoding("UTF-8"); 58 | 59 | //Automatic JSON serialization doesn't work yet in Portlet MVC 60 | //See: https://jira.spring.io/browse/SPR-7344 61 | JSON_MAPPER.writeValue(response.getPortletOutputStream(), users); 62 | } 63 | 64 | @ResourceMapping("userDetail") 65 | public void userDetail(@RequestParam long userId, ResourceResponse response) throws Exception { 66 | LOG.debug("Got detail request for user with id {}", userId); 67 | 68 | UserDetail userDetail = this.userService.getPortalUserDetail(userId); 69 | 70 | response.setContentType("application/json"); 71 | response.setCharacterEncoding("UTF-8"); 72 | 73 | JSON_MAPPER.writeValue(response.getPortletOutputStream(), userDetail); 74 | } 75 | 76 | private String getPortletId(PortletRequest request) { 77 | ThemeDisplay themeDisplay = (ThemeDisplay) request.getAttribute(WebKeys.THEME_DISPLAY); 78 | PortletDisplay portletDisplay = themeDisplay.getPortletDisplay(); 79 | return portletDisplay.getId(); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/main/java/at/nonblocking/portlet/angularjs/model/User.java: -------------------------------------------------------------------------------- 1 | package at.nonblocking.portlet.angularjs.model; 2 | 3 | public class User { 4 | 5 | private long userId; 6 | private String screenName; 7 | private String firstName; 8 | private String lastName; 9 | private String emailAddress; 10 | 11 | public User() { 12 | } 13 | 14 | public User(long userId, String screenName, String firstName, String lastName, String emailAddress) { 15 | this.userId = userId; 16 | this.screenName = screenName; 17 | this.firstName = firstName; 18 | this.lastName = lastName; 19 | this.emailAddress = emailAddress; 20 | } 21 | 22 | public long getUserId() { 23 | return userId; 24 | } 25 | 26 | public void setUserId(long userId) { 27 | this.userId = userId; 28 | } 29 | 30 | public String getScreenName() { 31 | return screenName; 32 | } 33 | 34 | public void setScreenName(String screenName) { 35 | this.screenName = screenName; 36 | } 37 | 38 | public String getFirstName() { 39 | return firstName; 40 | } 41 | 42 | public void setFirstName(String firstName) { 43 | this.firstName = firstName; 44 | } 45 | 46 | public String getLastName() { 47 | return lastName; 48 | } 49 | 50 | public void setLastName(String lastName) { 51 | this.lastName = lastName; 52 | } 53 | 54 | public String getEmailAddress() { 55 | return emailAddress; 56 | } 57 | 58 | public void setEmailAddress(String emailAddress) { 59 | this.emailAddress = emailAddress; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/main/java/at/nonblocking/portlet/angularjs/model/UserDetail.java: -------------------------------------------------------------------------------- 1 | package at.nonblocking.portlet.angularjs.model; 2 | 3 | import java.util.Date; 4 | 5 | public class UserDetail { 6 | 7 | private String screenName; 8 | private String firstName; 9 | private String lastName; 10 | private String emailAddress; 11 | private Date lastLoginDate; 12 | private String lastLoginIp; 13 | private String languageId; 14 | private String userGroups; 15 | private String roles; 16 | 17 | public String getScreenName() { 18 | return screenName; 19 | } 20 | 21 | public void setScreenName(String screenName) { 22 | this.screenName = screenName; 23 | } 24 | 25 | public String getFirstName() { 26 | return firstName; 27 | } 28 | 29 | public void setFirstName(String firstName) { 30 | this.firstName = firstName; 31 | } 32 | 33 | public String getLastName() { 34 | return lastName; 35 | } 36 | 37 | public void setLastName(String lastName) { 38 | this.lastName = lastName; 39 | } 40 | 41 | public String getEmailAddress() { 42 | return emailAddress; 43 | } 44 | 45 | public void setEmailAddress(String emailAddress) { 46 | this.emailAddress = emailAddress; 47 | } 48 | 49 | public Date getLastLoginDate() { 50 | return lastLoginDate; 51 | } 52 | 53 | public void setLastLoginDate(Date lastLoginDate) { 54 | this.lastLoginDate = lastLoginDate; 55 | } 56 | 57 | public String getLastLoginIp() { 58 | return lastLoginIp; 59 | } 60 | 61 | public void setLastLoginIp(String lastLoginIp) { 62 | this.lastLoginIp = lastLoginIp; 63 | } 64 | 65 | public String getLanguageId() { 66 | return languageId; 67 | } 68 | 69 | public void setLanguageId(String languageId) { 70 | this.languageId = languageId; 71 | } 72 | 73 | public String getUserGroups() { 74 | return userGroups; 75 | } 76 | 77 | public void setUserGroups(String userGroups) { 78 | this.userGroups = userGroups; 79 | } 80 | 81 | public String getRoles() { 82 | return roles; 83 | } 84 | 85 | public void setRoles(String roles) { 86 | this.roles = roles; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/main/java/at/nonblocking/portlet/angularjs/model/UserList.java: -------------------------------------------------------------------------------- 1 | package at.nonblocking.portlet.angularjs.model; 2 | 3 | import java.util.List; 4 | 5 | public class UserList { 6 | 7 | private int total; 8 | private List users; 9 | 10 | public UserList() { 11 | 12 | } 13 | 14 | public UserList(int total, List users) { 15 | this.total = total; 16 | this.users = users; 17 | } 18 | 19 | public List getUsers() { 20 | return users; 21 | } 22 | 23 | public void setUsers(List users) { 24 | this.users = users; 25 | } 26 | 27 | public int getTotal() { 28 | return total; 29 | } 30 | 31 | public void setTotal(int total) { 32 | this.total = total; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/at/nonblocking/portlet/angularjs/model/UserListRequest.java: -------------------------------------------------------------------------------- 1 | package at.nonblocking.portlet.angularjs.model; 2 | 3 | 4 | public class UserListRequest { 5 | 6 | private int startIndex; 7 | private int limit; 8 | 9 | public int getStartIndex() { 10 | return startIndex; 11 | } 12 | 13 | public void setStartIndex(int startIndex) { 14 | this.startIndex = startIndex; 15 | } 16 | 17 | public int getLimit() { 18 | return limit; 19 | } 20 | 21 | public void setLimit(int limit) { 22 | this.limit = limit; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/at/nonblocking/portlet/angularjs/service/UserService.java: -------------------------------------------------------------------------------- 1 | package at.nonblocking.portlet.angularjs.service; 2 | 3 | 4 | import at.nonblocking.portlet.angularjs.model.UserDetail; 5 | import at.nonblocking.portlet.angularjs.model.UserList; 6 | import com.liferay.portal.kernel.exception.PortalException; 7 | import com.liferay.portal.kernel.exception.SystemException; 8 | 9 | public interface UserService { 10 | 11 | UserList getPortalUserList(int startIndex, int limit) throws SystemException; 12 | 13 | UserDetail getPortalUserDetail(long userId) throws SystemException, PortalException; 14 | 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/at/nonblocking/portlet/angularjs/service/UserServiceImpl.java: -------------------------------------------------------------------------------- 1 | package at.nonblocking.portlet.angularjs.service; 2 | 3 | import at.nonblocking.portlet.angularjs.model.User; 4 | import at.nonblocking.portlet.angularjs.model.UserDetail; 5 | import at.nonblocking.portlet.angularjs.model.UserList; 6 | import com.liferay.portal.kernel.exception.PortalException; 7 | import com.liferay.portal.kernel.exception.SystemException; 8 | import com.liferay.portal.model.Role; 9 | import com.liferay.portal.model.UserGroup; 10 | import com.liferay.portal.service.UserLocalService; 11 | import com.liferay.portal.service.UserLocalServiceUtil; 12 | import org.apache.commons.lang3.StringUtils; 13 | 14 | import javax.inject.Named; 15 | import java.util.ArrayList; 16 | import java.util.List; 17 | 18 | @Named 19 | public class UserServiceImpl implements UserService { 20 | 21 | private UserLocalService liferayUserService; 22 | 23 | public UserServiceImpl(UserLocalService liferayUserService) { 24 | this.liferayUserService = liferayUserService; 25 | } 26 | 27 | public UserServiceImpl() { 28 | this(UserLocalServiceUtil.getService()); 29 | } 30 | 31 | @Override 32 | public UserList getPortalUserList(int startIndex, int limit) throws SystemException { 33 | int usersTotal = this.liferayUserService.getUsersCount(); 34 | int start = Math.min(startIndex, usersTotal); 35 | int end = Math.min(startIndex + limit, usersTotal); 36 | 37 | List portalUsers = this.liferayUserService.getUsers(start, end); 38 | List users = new ArrayList<>(); 39 | 40 | for (com.liferay.portal.model.User portalUser : portalUsers) { 41 | users.add(new User(portalUser.getUserId(), portalUser.getScreenName(), portalUser.getFirstName(), portalUser.getLastName(), portalUser.getEmailAddress())); 42 | } 43 | 44 | return new UserList(usersTotal, users); 45 | } 46 | 47 | 48 | public UserDetail getPortalUserDetail(long userId) throws SystemException, PortalException { 49 | com.liferay.portal.model.User liferayUser = this.liferayUserService.getUser(userId); 50 | 51 | UserDetail userDetail = new UserDetail(); 52 | userDetail.setScreenName(liferayUser.getScreenName()); 53 | userDetail.setEmailAddress(liferayUser.getEmailAddress()); 54 | userDetail.setFirstName(liferayUser.getFirstName()); 55 | userDetail.setLastName(liferayUser.getLastName()); 56 | userDetail.setLastLoginDate(liferayUser.getLastLoginDate()); 57 | userDetail.setLastLoginIp(liferayUser.getLastName()); 58 | userDetail.setLanguageId(liferayUser.getLanguageId()); 59 | 60 | userDetail.setUserGroups(toCommaSeparatedUserGroupList(liferayUser)); 61 | userDetail.setRoles(toCommaSeparatedRoleList(liferayUser)); 62 | 63 | return userDetail; 64 | } 65 | 66 | private String toCommaSeparatedUserGroupList(com.liferay.portal.model.User liferayUser)throws SystemException { 67 | List userGroupNames = new ArrayList<>(); 68 | for (UserGroup userGroup : liferayUser.getUserGroups()) { 69 | userGroupNames.add(userGroup.getName()); 70 | } 71 | return StringUtils.join(userGroupNames, ","); 72 | } 73 | 74 | private String toCommaSeparatedRoleList(com.liferay.portal.model.User liferayUser)throws SystemException { 75 | List roleNames = new ArrayList<>(); 76 | for (Role role : liferayUser.getRoles()) { 77 | roleNames.add(role.getName()); 78 | } 79 | return StringUtils.join(roleNames, ","); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/main/javascript/app.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | angular.module('nonblocking.ng1.portletDemo', [ 4 | 'nonblocking.ng1.portletDemo.mainController', 5 | 'nonblocking.ng1.portletDemo.userListController', 6 | 'nonblocking.ng1.portletDemo.userDetailController', 7 | 'nonblocking.ng1.portletDemo.router', 8 | 'nonblocking.ng1.portletDemo.routerView' ]) 9 | 10 | .config(function(routerProvider, portletConfig) { 11 | 12 | routerProvider.config([ 13 | { name: "userList", url: portletConfig.portletAppContextPath + "templates/userList.html" }, 14 | { name: "userDetail", url: portletConfig.portletAppContextPath + "templates/userDetail.html" } 15 | ]); 16 | }); -------------------------------------------------------------------------------- /src/main/javascript/controllers/main_controller.js: -------------------------------------------------------------------------------- 1 | 2 | angular.module('nonblocking.ng1.portletDemo.mainController', []) 3 | 4 | .controller('MainController', function ($scope, router, portletConfig) { 5 | $scope.authenticatedUser = portletConfig.authenticatedUser; 6 | 7 | }); -------------------------------------------------------------------------------- /src/main/javascript/controllers/user_detail_controller.js: -------------------------------------------------------------------------------- 1 | 2 | angular.module('nonblocking.ng1.portletDemo.userDetailController', [ 'nonblocking.ng1.portletDemo.backend' ]) 3 | 4 | .controller('UserDetailController', function ($scope, backend, router) { 5 | $scope.selectedUser = null; 6 | 7 | $scope.showList = function() { 8 | router.goto('userList'); 9 | }; 10 | 11 | $scope.loadDetails = function() { 12 | backend.userDetail(router.getParams().selectedUserId).then( 13 | function(response) { 14 | $scope.selectedUser = response.data; 15 | }, function(response) { 16 | alert("Error: " + response.status); 17 | }); 18 | }; 19 | 20 | $scope.loadDetails(); 21 | 22 | }); -------------------------------------------------------------------------------- /src/main/javascript/controllers/user_list_controller.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | angular.module('nonblocking.ng1.portletDemo.userListController', [ 'nonblocking.ng1.portletDemo.backend' ]) 5 | 6 | .controller('UserListController', function ($scope, backend, router) { 7 | $scope.entriesPerPage = 10; 8 | $scope.currentPage = 0; 9 | $scope.totalEntries = 0; 10 | $scope.users = []; 11 | 12 | $scope.totalPages = function() { 13 | return Math.floor($scope.totalEntries / $scope.entriesPerPage) + 1; 14 | }; 15 | 16 | $scope.isLastPage = function() { 17 | return $scope.totalEntries < ($scope.currentPage + 1) * $scope.entriesPerPage; 18 | }; 19 | 20 | $scope.isFirstPage = function() { 21 | return $scope.currentPage === 0; 22 | }; 23 | 24 | $scope.nextPage = function() { 25 | if (!$scope.isLastPage()) { 26 | $scope.currentPage += 1; 27 | $scope.loadUsers(); 28 | } 29 | }; 30 | 31 | $scope.previousPage = function() { 32 | if (!$scope.isFirstPage()) { 33 | $scope.currentPage -= 1; 34 | $scope.loadUsers(); 35 | } 36 | }; 37 | 38 | $scope.showDetails = function(userId) { 39 | router.goto('userDetail', { 40 | selectedUserId: userId 41 | }); 42 | }; 43 | 44 | $scope.loadUsers = function() { 45 | backend.userList( $scope.currentPage * $scope.entriesPerPage, $scope.entriesPerPage).then( 46 | function(response) { 47 | $scope.totalEntries = response.data.total; 48 | $scope.users = response.data.users; 49 | }, function(response) { 50 | alert("Error: " + response.status); 51 | }); 52 | }; 53 | 54 | $scope.loadUsers(); 55 | 56 | }); -------------------------------------------------------------------------------- /src/main/javascript/directives/router_view.js: -------------------------------------------------------------------------------- 1 | 2 | angular.module('nonblocking.ng1.portletDemo.routerView', []) 3 | 4 | .directive('routerView', function(router) { 5 | return { 6 | restrict: 'E', 7 | controller: function($scope) { 8 | $scope.getTemplateUrl = function() { 9 | return router.getTemplateUrl(); 10 | } 11 | }, 12 | template: '
' 13 | } 14 | }); -------------------------------------------------------------------------------- /src/main/javascript/services/backend.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | angular.module('nonblocking.ng1.portletDemo.backend', []) 4 | 5 | .factory('backend', function($http, $httpParamSerializerJQLike, portletConfig) { 6 | 7 | var PortletBackend = function() { 8 | this.userList = function(startIndex, limit) { 9 | return ajaxPost("userList", { "startIndex": startIndex, "limit": limit }); 10 | }; 11 | 12 | this.userDetail = function(userId) { 13 | return ajaxPost("userDetail", { "userId": userId }); 14 | }; 15 | 16 | function ajaxPost(method, data){ 17 | return $http({ 18 | url: portletConfig.ajaxUrl + "&p_p_resource_id=" + method, 19 | method: 'POST', 20 | headers: {'Content-Type': 'application/x-www-form-urlencoded'}, 21 | data: $httpParamSerializerJQLike(data) 22 | }); 23 | } 24 | }; 25 | 26 | var TestBackend = function() { 27 | this.userList = function(startIndex, limit) { 28 | var jsonFile = startIndex === 0 ? 'users.json' : 'users2.json'; 29 | 30 | return $http({ 31 | url: portletConfig.ajaxUrl + jsonFile, 32 | method: 'GET' 33 | }); 34 | }; 35 | 36 | this.userDetail = function(userId) { 37 | return $http({ 38 | url: portletConfig.ajaxUrl + 'userDetails.json', 39 | method: 'GET' 40 | }); 41 | }; 42 | }; 43 | 44 | if (portletConfig.isStandalone) { 45 | return new TestBackend(); 46 | } else { 47 | return new PortletBackend(); 48 | } 49 | }); -------------------------------------------------------------------------------- /src/main/javascript/services/router.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | angular.module('nonblocking.ng1.portletDemo.router', []) 4 | 5 | .provider('router', function() { 6 | 7 | var templates = []; 8 | var currentTemplate = null; 9 | var params = null; 10 | 11 | this.config = function(ts) { 12 | templates = ts; 13 | if (!currentTemplate && templates && templates.length > 0) { 14 | currentTemplate = templates[0]; 15 | } 16 | }; 17 | 18 | this.$get = function() { 19 | return { 20 | goto: function (name, ps) { 21 | angular.forEach(templates, function (template) { 22 | if (template.name === name) { 23 | currentTemplate = template; 24 | } 25 | }); 26 | if (ps) { 27 | params = ps; 28 | } else { 29 | params = {}; 30 | } 31 | }, 32 | 33 | getParams: function() { 34 | return params; 35 | }, 36 | 37 | getTemplateUrl: function () { 38 | return currentTemplate.url; 39 | } 40 | } 41 | }; 42 | }); 43 | -------------------------------------------------------------------------------- /src/main/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n 7 | 8 | 9 | 10 | 11 | ${liferay.home}/logs/angularjs-portlet.log 12 | 13 | %d{ISO8601} [%thread] %-5level %logger{36} - %msg%n 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/main/sass/app.scss: -------------------------------------------------------------------------------- 1 | 2 | $COLOR_FONT: #343333; 3 | $COLOR_RED: #923132; 4 | 5 | .ng1-portlet-demo { 6 | color: $COLOR_FONT; 7 | 8 | a { 9 | color: $COLOR_FONT; 10 | text-decoration: underline; 11 | padding: 5px 0; 12 | 13 | &:hover { 14 | color: $COLOR_RED; 15 | } 16 | 17 | &.disabled { 18 | &,&:hover { 19 | text-decoration: none; 20 | color: #999; 21 | cursor: default; 22 | } 23 | } 24 | } 25 | 26 | .ng1-portlet-user-table { 27 | border-collapse: collapse; 28 | border: 1px solid #999; 29 | 30 | tr { 31 | &.even { 32 | background: #eee; 33 | } 34 | } 35 | 36 | th { 37 | padding: 5px; 38 | text-align: left; 39 | border-bottom: 1px solid #999; 40 | } 41 | 42 | td { 43 | padding: 5px; 44 | } 45 | } 46 | } 47 | 48 | -------------------------------------------------------------------------------- /src/main/webapp/WEB-INF/liferay-display.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/main/webapp/WEB-INF/liferay-plugin-package.properties: -------------------------------------------------------------------------------- 1 | name=Liferay AngularJS Demo 2 | module-group-id=nonblocking.at 3 | module-incremental-version=2 4 | tags=HTML5,AngularJS 5 | short-description=Liferay AngularJS Demo 6 | change-log= 7 | page-url=http://www.nonblocking.at 8 | author=nonblocking.at gmbh 9 | licenses=Apache License, Version 2.0 10 | liferay-versions=6.2.0+ 11 | speed-filters-enabled=false 12 | -------------------------------------------------------------------------------- /src/main/webapp/WEB-INF/liferay-portlet.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | liferay-angularjs-portlet 7 | true 8 | true 9 | 13 | false 14 | true 15 | /css/app.css 16 | /js/vendor.js 17 | /js/app.js 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/main/webapp/WEB-INF/portlet.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | liferay-angularjs-portlet 9 | AngularJS Portlet Demo 10 | org.springframework.web.portlet.DispatcherPortlet 11 | 12 | contextConfigLocation 13 | /WEB-INF/spring/portlet-context.xml 14 | 15 | 0 16 | 17 | text/html 18 | 19 | 20 | Liferay AngularJS Demo 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/main/webapp/WEB-INF/spring/portlet-context.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/main/webapp/WEB-INF/spring/root-context.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/main/webapp/WEB-INF/web.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | contextConfigLocation 8 | /WEB-INF/spring/root-context.xml 9 | 10 | 11 | 12 | org.springframework.web.context.ContextLoaderListener 13 | 14 | 15 | 16 | ViewRendererServlet 17 | org.springframework.web.servlet.ViewRendererServlet 18 | 19 | 20 | 21 | ViewRendererServlet 22 | /WEB-INF/servlet/view 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/main/webapp/index.html: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | AngularJS Portlet Demo 6 | 7 | 8 | 9 | 10 | 11 |
12 |

AngularJS Portlet Demo v3

13 |

Authenticated User: {{authenticatedUser}}

14 | Loading... 15 |
16 | 17 | 18 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /src/main/webapp/templates/userDetail.html: -------------------------------------------------------------------------------- 1 |
2 |

Portal User: {{selectedUser.screenName}}

3 |
4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 |
Screen Name:{{selectedUser.screenName}}
EMail Address:{{selectedUser.emailAddress}}
First Name:{{selectedUser.firstName}}
Last Name:{{selectedUser.lastName}}
Last Login Date:{{selectedUser.lastLoginDate | date:'yyyy-MM-dd'}}
Last Login IP:{{selectedUser.lastLoginIp}}
Language:{{selectedUser.languageId}}
User Groups:{{selectedUser.userGroups}}
Roles:{{selectedUser.roles}}
42 |
43 |
44 | Back 45 |
46 |
47 | -------------------------------------------------------------------------------- /src/main/webapp/templates/userList.html: -------------------------------------------------------------------------------- 1 |
2 |

Portal User List

3 |
4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
Screen NameFirst NameLast NameEMail Address
{{user.screenName}}{{user.firstName}}{{user.lastName}}{{user.emailAddress}}Details
20 |
21 |
22 | Page {{currentPage + 1}} of {{totalPages()}} 23 |   24 | Previous Page 25 | | 26 | Next Page 27 |
28 |
-------------------------------------------------------------------------------- /src/main/webapp/testdata/userDetails.json: -------------------------------------------------------------------------------- 1 | { 2 | "screenName": "test1", 3 | "firstName": "First1", 4 | "lastName": "Last1", 5 | "emailAddress": "test1@nonblocking.at", 6 | "lastLoginDate": 1411809626460, 7 | "lastLoginIp": "127.0.0.1", 8 | "languageId": "en_US", 9 | "userGroups": "Group1", 10 | "roles": "User,Power User" 11 | } 12 | 13 | -------------------------------------------------------------------------------- /src/main/webapp/testdata/users.json: -------------------------------------------------------------------------------- 1 | { 2 | "total": 13, 3 | "users": [ 4 | { 5 | "userId": 0, 6 | "screenName": "test1", 7 | "firstName": "First1", 8 | "lastName": "Last1", 9 | "emailAddress": "test1@nonblocking.at" 10 | }, 11 | { 12 | "userId": 1, 13 | "screenName": "test2", 14 | "firstName": "First2", 15 | "lastName": "Last2", 16 | "emailAddress": "test2@nonblocking.at" 17 | }, 18 | { 19 | "userId": 2, 20 | "screenName": "test3", 21 | "firstName": "First3", 22 | "lastName": "Last3", 23 | "emailAddress": "test3@nonblocking.at" 24 | }, 25 | { 26 | "userId": 3, 27 | "screenName": "test4", 28 | "firstName": "First4", 29 | "lastName": "Last4", 30 | "emailAddress": "test4@nonblocking.at" 31 | }, 32 | { 33 | "userId": 4, 34 | "screenName": "test5", 35 | "firstName": "First5", 36 | "lastName": "Last5", 37 | "emailAddress": "test5@nonblocking.at" 38 | }, 39 | { 40 | "userId": 5, 41 | "screenName": "test6", 42 | "firstName": "First6", 43 | "lastName": "Last6", 44 | "emailAddress": "test6@nonblocking.at" 45 | }, 46 | { 47 | "userId": 6, 48 | "screenName": "test7", 49 | "firstName": "First7", 50 | "lastName": "Last7", 51 | "emailAddress": "test3@nonblocking.at" 52 | }, 53 | { 54 | "userId": 7, 55 | "screenName": "test8", 56 | "firstName": "First8", 57 | "lastName": "Last8", 58 | "emailAddress": "test3@nonblocking.at" 59 | }, 60 | { 61 | "userId": 8, 62 | "screenName": "test9", 63 | "firstName": "First9", 64 | "lastName": "Last9", 65 | "emailAddress": "test9@nonblocking.at" 66 | }, 67 | { 68 | "userId": 9, 69 | "screenName": "test10", 70 | "firstName": "First10", 71 | "lastName": "Last10", 72 | "emailAddress": "test10@nonblocking.at" 73 | } 74 | ] 75 | } 76 | 77 | -------------------------------------------------------------------------------- /src/main/webapp/testdata/users2.json: -------------------------------------------------------------------------------- 1 | { 2 | "total": 13, 3 | "users": [ 4 | { 5 | "userId": 10, 6 | "screenName": "test11", 7 | "firstName": "First11", 8 | "lastName": "Last11", 9 | "emailAddress": "test11@nonblocking.at" 10 | }, 11 | { 12 | "userId": 11, 13 | "screenName": "test12", 14 | "firstName": "First12", 15 | "lastName": "Last12", 16 | "emailAddress": "test12@nonblocking.at" 17 | }, 18 | { 19 | "userId": 12, 20 | "screenName": "test13", 21 | "firstName": "First13", 22 | "lastName": "Last13", 23 | "emailAddress": "test13@nonblocking.at" 24 | } 25 | ] 26 | } 27 | 28 | --------------------------------------------------------------------------------