Browse Source

Merge pull request #92 from jedwardhawkins/master

Latest PeopleSearch and Orgchart changes
Jason 8 years ago
parent
commit
bf1ea46de1
85 changed files with 3122 additions and 731 deletions
  1. 5 0
      pom.xml
  2. 1 0
      src/main/angular/images/icons/m_edit.svg
  3. 0 1
      src/main/angular/images/icons/m_file-tree.svg
  4. 1 0
      src/main/angular/images/icons/m_magnify.svg
  5. 25 0
      src/main/angular/images/icons/m_next-right.svg
  6. 1 0
      src/main/angular/images/icons/m_orgchart.svg
  7. 0 1
      src/main/angular/images/icons/m_table-large.svg
  8. 0 1
      src/main/angular/images/icons/m_view-grid.svg
  9. 1 0
      src/main/angular/images/icons/m_view-list.svg
  10. 1 0
      src/main/angular/images/icons/m_view-tile.svg
  11. 23 0
      src/main/angular/karma.conf.js
  12. 29 1
      src/main/angular/src/component.ts
  13. 9 2
      src/main/angular/src/i18n/translations_en.json
  14. 5 0
      src/main/angular/src/icons.json
  15. 24 0
      src/main/angular/src/main.dev.ts
  16. 40 8
      src/main/angular/src/main.ts
  17. 23 0
      src/main/angular/src/models/column.model.ts
  18. 23 1
      src/main/angular/src/models/orgchart-data.model.ts
  19. 26 3
      src/main/angular/src/models/person.model.ts
  20. 34 0
      src/main/angular/src/models/search-result.model.ts
  21. 17 5
      src/main/angular/src/peoplesearch/orgchart-search.component.html
  22. 28 43
      src/main/angular/src/peoplesearch/orgchart-search.component.scss
  23. 48 6
      src/main/angular/src/peoplesearch/orgchart-search.component.ts
  24. 8 3
      src/main/angular/src/peoplesearch/orgchart.component.html
  25. 36 13
      src/main/angular/src/peoplesearch/orgchart.component.scss
  26. 22 2
      src/main/angular/src/peoplesearch/orgchart.component.test.ts
  27. 47 75
      src/main/angular/src/peoplesearch/orgchart.component.ts
  28. 137 14
      src/main/angular/src/peoplesearch/peoplesearch-base.component.ts
  29. 24 7
      src/main/angular/src/peoplesearch/peoplesearch-cards.component.html
  30. 61 23
      src/main/angular/src/peoplesearch/peoplesearch-cards.component.scss
  31. 51 25
      src/main/angular/src/peoplesearch/peoplesearch-cards.component.ts
  32. 34 27
      src/main/angular/src/peoplesearch/peoplesearch-table.component.html
  33. 25 2
      src/main/angular/src/peoplesearch/peoplesearch-table.component.scss
  34. 31 23
      src/main/angular/src/peoplesearch/peoplesearch-table.component.ts
  35. 28 3
      src/main/angular/src/peoplesearch/peoplesearch.module.ts
  36. 38 8
      src/main/angular/src/peoplesearch/peoplesearch.scss
  37. 3 2
      src/main/angular/src/peoplesearch/person-card.component.html
  38. 50 11
      src/main/angular/src/peoplesearch/person-card.component.scss
  39. 57 12
      src/main/angular/src/peoplesearch/person-card.component.ts
  40. 51 0
      src/main/angular/src/peoplesearch/person-details-dialog.component.html
  41. 95 0
      src/main/angular/src/peoplesearch/person-details-dialog.component.scss
  42. 76 0
      src/main/angular/src/peoplesearch/person-details-dialog.component.ts
  43. 23 0
      src/main/angular/src/peoplesearch/person.filters.ts
  44. 34 7
      src/main/angular/src/peoplesearch/string.filters.ts
  45. 39 3
      src/main/angular/src/routes.ts
  46. 26 6
      src/main/angular/src/services/config.service.dev.ts
  47. 29 5
      src/main/angular/src/services/config.service.ts
  48. 202 145
      src/main/angular/src/services/people.data.json
  49. 73 19
      src/main/angular/src/services/people.service.dev.ts
  50. 68 36
      src/main/angular/src/services/people.service.ts
  51. 85 9
      src/main/angular/src/services/pwm.service.ts
  52. 38 14
      src/main/angular/src/services/translations-loader.factory.ts
  53. 62 8
      src/main/angular/src/ux/app-bar.component.scss
  54. 36 0
      src/main/angular/src/ux/app-bar.component.ts
  55. 0 5
      src/main/angular/src/ux/auto-complete.component.html
  56. 30 2
      src/main/angular/src/ux/auto-complete.component.scss
  57. 125 53
      src/main/angular/src/ux/auto-complete.component.ts
  58. 47 0
      src/main/angular/src/ux/button.component.scss
  59. 32 0
      src/main/angular/src/ux/button.component.ts
  60. 6 0
      src/main/angular/src/ux/dialog.component.html
  61. 90 0
      src/main/angular/src/ux/dialog.component.scss
  62. 61 0
      src/main/angular/src/ux/dialog.component.ts
  63. 116 0
      src/main/angular/src/ux/dialog.service.ts
  64. 125 0
      src/main/angular/src/ux/element-size.service.ts
  65. 46 9
      src/main/angular/src/ux/icon-button.component.scss
  66. 24 1
      src/main/angular/src/ux/icon-button.component.ts
  67. 25 2
      src/main/angular/src/ux/icon.component.scss
  68. 23 0
      src/main/angular/src/ux/icon.component.ts
  69. 6 3
      src/main/angular/src/ux/search-bar.component.html
  70. 55 14
      src/main/angular/src/ux/search-bar.component.scss
  71. 49 18
      src/main/angular/src/ux/search-bar.component.ts
  72. 23 0
      src/main/angular/src/ux/table-column.directive.ts
  73. 70 25
      src/main/angular/src/ux/table.directive.controller.ts
  74. 7 7
      src/main/angular/src/ux/table.directive.html
  75. 25 0
      src/main/angular/src/ux/table.directive.scss
  76. 40 7
      src/main/angular/src/ux/table.directive.ts
  77. 32 1
      src/main/angular/src/ux/ux.module.ts
  78. 5 0
      src/main/angular/tslint.json
  79. 33 1
      src/main/angular/webpack.build.js
  80. 24 7
      src/main/angular/webpack.common.js
  81. 24 0
      src/main/angular/webpack.dev.js
  82. 23 0
      src/main/angular/webpack.test.js
  83. 1 1
      src/main/resources/password/pwm/AppProperty.properties
  84. 1 0
      src/main/webapp/WEB-INF/jsp/peoplesearch.jsp
  85. 1 1
      src/main/webapp/private/index.jsp

+ 5 - 0
pom.xml

@@ -747,6 +747,11 @@
             <artifactId>angular-translate</artifactId>
             <version>2.13.0</version>
         </dependency>
+        <dependency>
+            <groupId>org.webjars.npm</groupId>
+            <artifactId>angular-sanitize</artifactId>
+            <version>1.5.8</version>
+        </dependency>
     </dependencies>
 
     <repositories>

+ 1 - 0
src/main/angular/images/icons/m_edit.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 72 72"><title>m_edit</title><path d="M26.4,41.73L23.8,47.26a0.61,0.61,0,0,0,.58.82l0.17,0,5.6-2.5a0.62,0.62,0,0,0,.27-0.16L63.2,12.73a0.61,0.61,0,0,0,0-.86l-3-3.14a0.61,0.61,0,0,0-.44-0.19,0.51,0.51,0,0,0-.44.18L53,15H13.53a4,4,0,0,0-4,4V61.2a4,4,0,0,0,4,4H54.38a4,4,0,0,0,4-4v-39l-4,4.06V61.19H13.55l0-42.15H49.05L26.54,41.5A0.61,0.61,0,0,0,26.4,41.73Z" fill="gray"/></svg>

+ 0 - 1
src/main/angular/images/icons/m_file-tree.svg

@@ -1 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M3,3H9V7H3V3M15,10H21V14H15V10M15,17H21V21H15V17M13,13H7V18H13V20H7L5,20V9H7V11H13V13Z" /></svg>

+ 1 - 0
src/main/angular/images/icons/m_magnify.svg

@@ -0,0 +1 @@
+<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M9.5,3C13.09,3 16,5.91 16,9.5C16,11.11 15.41,12.59 14.44,13.73L14.71,14H15.5L20.5,19L19,20.5L14,15.5V14.71L13.73,14.44C12.59,15.41 11.11,16 9.5,16C5.91,16 3,13.09 3,9.5C3,5.91 5.91,3 9.5,3M9.5,5C7,5 5,7 5,9.5C5,12 7,14 9.5,14C12,14 14,12 14,9.5C14,7 12,5 9.5,5Z" /></svg>

+ 25 - 0
src/main/angular/images/icons/m_next-right.svg

@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 19.2.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="72px"
+	 height="72px" viewBox="109 487 72 72" style="enable-background:new 109 487 72 72;" xml:space="preserve">
+<style type="text/css">
+	.st0{fill:#808080;}
+	.st1{fill:#3DD77A;}
+	.st2{fill:#E50000;}
+	.st3{fill:#FFBC00;}
+	.st4{fill:#FF8578;}
+	.st5{fill-rule:evenodd;clip-rule:evenodd;fill:#FF8575;}
+	.st6{fill-rule:evenodd;clip-rule:evenodd;fill:#808080;}
+	.st7{fill:none;stroke:#808080;stroke-width:4;stroke-miterlimit:10;}
+	.st8{display:none;}
+	.st9{display:inline;fill:none;}
+</style>
+<g id="SVG_icons">
+	<g id="right">
+		<polygon class="st0" points="133.77,551.12 130.94,548.29 156.5,522.73 131.16,497.38 133.99,494.56 162.16,522.73 		"/>
+	</g>
+</g>
+<g id="Rectangles" class="st8">
+	<rect x="109.36" y="487.36" class="st9" width="71.28" height="71.28"/>
+</g>
+</svg>

+ 1 - 0
src/main/angular/images/icons/m_orgchart.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 72 72"><title>m_brand</title><path d="M65,43.46H55.4V35.75a1,1,0,0,0-1-1H37.48V28.36H47.92a1.5,1.5,0,0,0,1.5-1.5V8.91a1.5,1.5,0,0,0-1.5-1.5H25.18a1.5,1.5,0,0,0-1.5,1.5V26.86a1.5,1.5,0,0,0,1.5,1.5H35.48v6.39H18.55a1,1,0,0,0-1,1v7.71H7.93A1.5,1.5,0,0,0,6.43,45V62.9a1.5,1.5,0,0,0,1.5,1.5H29.17a1.5,1.5,0,0,0,1.5-1.5V45a1.5,1.5,0,0,0-1.5-1.5H19.55V36.75H53.4v6.71H43.78a1.5,1.5,0,0,0-1.5,1.5V62.9a1.5,1.5,0,0,0,1.5,1.5H65a1.5,1.5,0,0,0,1.5-1.5V45A1.5,1.5,0,0,0,65,43.46Zm-38.34-33H46.42V25.36H26.68V10.41Zm1,51H9.43V46.46H27.67V61.4Zm35.85,0H45.28V46.46H63.52V61.4Z" fill="gray"/></svg>

+ 0 - 1
src/main/angular/images/icons/m_table-large.svg

@@ -1 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M4,3H20C21.1,3 22,3.9 22,5V20C22,21.1 21.1,22 20,22H4C2.9,22 2,21.1 2,20V5C2,3.9 2.9,3 4,3M4,7V10H8V7H4M10,7V10H14V7H10M20,10V7H16V10H20M4,12V15H8V12H4M4,20H8V17H4V20M10,12V15H14V12H10M10,20H14V17H10V20M20,20V17H16V20H20M20,12H16V15H20V12Z" /></svg>

+ 0 - 1
src/main/angular/images/icons/m_view-grid.svg

@@ -1 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M3,11H11V3H3M3,21H11V13H3M13,21H21V13H13M13,3V11H21V3" /></svg>

+ 1 - 0
src/main/angular/images/icons/m_view-list.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 72 72"><title>1-icons_expanded</title><path d="M63,17.65H8.9a1.5,1.5,0,0,1-1.5-1.5V9a1.5,1.5,0,0,1,1.5-1.5H63A1.5,1.5,0,0,1,64.49,9v7.14A1.5,1.5,0,0,1,63,17.65Zm-52.59-3H61.49V10.51H10.4v4.14Z" fill="gray"/><path d="M63,64.45H8.9A1.5,1.5,0,0,1,7.4,63V55.81a1.5,1.5,0,0,1,1.5-1.5H63a1.5,1.5,0,0,1,1.5,1.5V63A1.5,1.5,0,0,1,63,64.45Zm-52.59-3H61.49V57.31H10.4v4.14Z" fill="gray"/><path d="M63,48.85H8.9a1.5,1.5,0,0,1-1.5-1.5V40.21a1.5,1.5,0,0,1,1.5-1.5H63a1.5,1.5,0,0,1,1.5,1.5v7.14A1.5,1.5,0,0,1,63,48.85Zm-52.59-3H61.49V41.71H10.4v4.14Z" fill="gray"/><path d="M63,33.25H8.9a1.5,1.5,0,0,1-1.5-1.5V24.61a1.5,1.5,0,0,1,1.5-1.5H63a1.5,1.5,0,0,1,1.5,1.5v7.14A1.5,1.5,0,0,1,63,33.25Zm-52.59-3H61.49V26.11H10.4v4.14Z" fill="gray"/></svg>

+ 1 - 0
src/main/angular/images/icons/m_view-tile.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 72 72"><title>1-icons_expanded</title><path d="M31.35,32.85H8.76a1.5,1.5,0,0,1-1.5-1.5V8.91a1.5,1.5,0,0,1,1.5-1.5H31.35a1.5,1.5,0,0,1,1.5,1.5V31.35A1.5,1.5,0,0,1,31.35,32.85Zm-21.09-3H29.85V10.41H10.26V29.85Z" fill="gray"/><path d="M63,32.85H40.41a1.5,1.5,0,0,1-1.5-1.5V8.91a1.5,1.5,0,0,1,1.5-1.5H63a1.5,1.5,0,0,1,1.5,1.5V31.35A1.5,1.5,0,0,1,63,32.85Zm-21.09-3H61.5V10.41H41.91V29.85Z" fill="gray"/><path d="M31.35,64.5H8.76A1.5,1.5,0,0,1,7.26,63V40.56a1.5,1.5,0,0,1,1.5-1.5H31.35a1.5,1.5,0,0,1,1.5,1.5V63A1.5,1.5,0,0,1,31.35,64.5Zm-21.09-3H29.85V42.06H10.26V61.5Z" fill="gray"/><path d="M63,64.5H40.41a1.5,1.5,0,0,1-1.5-1.5V40.56a1.5,1.5,0,0,1,1.5-1.5H63a1.5,1.5,0,0,1,1.5,1.5V63A1.5,1.5,0,0,1,63,64.5Zm-21.09-3H61.5V42.06H41.91V61.5Z" fill="gray"/></svg>

+ 23 - 0
src/main/angular/karma.conf.js

@@ -1,3 +1,26 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2016 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+
 var webpackConfig = require('./webpack.test.js');
 
 module.exports = function (config) {

+ 29 - 1
src/main/angular/src/component.ts

@@ -1,10 +1,38 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2016 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+
 import * as angular from 'angular';
+import { IAugmentedJQuery, IAttributes} from 'angular';
+
+export interface IContentTemplateFunction {
+    ($element: IAugmentedJQuery, $attrs?: IAttributes): string;
+}
 
 export function Component(options: {
     bindings?: any,
     bindToController?: boolean,
     controllerAs?: string,
-    template?: string,
+    template?: (string | any[] | IContentTemplateFunction),
     templateUrl?: string,
     transclude?: boolean,
     stylesheetUrl?: string

+ 9 - 2
src/main/angular/src/i18n/translations_en.json

@@ -1,8 +1,15 @@
 {
   "Title_PeopleSearch": "People Search",
   "Title_Management": "Management",
-  "Title_DirectReports": "Direct Reports",
+  "Title_DirectReports": "Direct Report(s)",
   "Title_Organization": "Organization",
+  "Title_OrgChart": "Organizational Chart",
+  "Title_Details": "Details",
 
-  "Display_PleaseWait": "Loading..."
+  "Display_PleaseWait": "Loading...",
+
+  "Display_SearchResultsExceeded": "Search results exceeded maximum search size.",
+  "Display_SearchResultsNone": "No results.",
+
+  "Placeholder_Search": "Search"
 }

+ 5 - 0
src/main/angular/src/icons.json

@@ -4,6 +4,11 @@
   "files": [ "../images/icons/**/*.svg" ],
   "fixedWidth": true,
   "fontName": "icons",
+  "formatOptions": {
+    "ttf": {
+      "ts": 1451512800000
+    }
+  },
   "html": true,
   "normalize": true,
   "types": [ "eot", "woff", "ttf", "svg" ]

+ 24 - 0
src/main/angular/src/main.dev.ts

@@ -1,3 +1,26 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2016 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+
 import { bootstrap, module } from 'angular';
 import ConfigService from './services/config.service.dev';
 import peopleSearchModule from './peoplesearch/peoplesearch.module';
@@ -9,6 +32,7 @@ import uiRouter from 'angular-ui-router';
 require('./icons.json');
 
 module('app', [
+    'ngSanitize',
     uiRouter,
     peopleSearchModule,
     'pascalprecht.translate'

+ 40 - 8
src/main/angular/src/main.ts

@@ -1,30 +1,62 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2016 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+
 import { bootstrap, module } from 'angular';
 import ConfigService from './services/config.service';
 import peopleSearchModule from './peoplesearch/peoplesearch.module';
 import PeopleService from './services/people.service';
+import PwmService from './services/pwm.service';
 import routes from './routes';
-import translationsLoader from './services/translations-loader.factory';
+import TranslationsLoaderFactory from './services/translations-loader.factory';
 import uiRouter from 'angular-ui-router';
 
 // fontgen-loader needs this :(
 require('./icons.json');
 
 module('app', [
+    'ngSanitize',
     uiRouter,
     peopleSearchModule,
     'pascalprecht.translate'
 ])
 
     .config(routes)
-    .config(['$translateProvider', ($translateProvider: angular.translate.ITranslateProvider) => {
-        $translateProvider.useLoader('translationsLoader');
-        $translateProvider.useSanitizeValueStrategy('escapeParameters');
-        $translateProvider.preferredLanguage('en');
-    }])
+    .config([
+        '$translateProvider',
+        ($translateProvider: angular.translate.ITranslateProvider) => {
+            $translateProvider
+                .translations('fallback', require('i18n/translations_en.json'))
+                .useLoader('translationsLoader')
+                .useSanitizeValueStrategy('escapeParameters')
+                .preferredLanguage('en')
+                .fallbackLanguage('fallback')
+                .forceAsyncReload(true);
+        }])
     .service('PeopleService', PeopleService)
+    .service('PwmService', PwmService)
     .service('ConfigService', ConfigService)
-    .factory('translationsLoader', translationsLoader);
+    .factory('translationsLoader', TranslationsLoaderFactory);
 
 // Attach to the page document
-bootstrap(document, ['app']);
+bootstrap(document, ['app'], { strictDi: true });
 

+ 23 - 0
src/main/angular/src/models/column.model.ts

@@ -1,3 +1,26 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2016 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+
 export default class Column {
     constructor(public label: string,
                 public valueExpression: string,

+ 23 - 1
src/main/angular/src/models/orgchart-data.model.ts

@@ -1,8 +1,30 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2016 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+
 import Person from './person.model';
 export default class OrgChartData {
 
     constructor(public manager: Person,
                 public children: Person[],
                 public self: Person) {}
-
 }

+ 26 - 3
src/main/angular/src/models/person.model.ts

@@ -1,10 +1,33 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2016 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+
 export default class Person {
     // Common properties
     userKey: string;
-    numOfDirectReports: number;
+    numDirectReports: number;
 
     // Autocomplete properties (via Search)
-    displayName: string;
+    _displayName: string;
 
     // Details properties (not available in search)
     detail: any;
@@ -23,7 +46,7 @@ export default class Person {
         this.userKey = options.userKey;
 
         // Autocomplete properties (via Search)
-        this.displayName = options.displayName;
+        this._displayName = options._displayName;
 
         // Details properties
         this.detail = options.detail;

+ 34 - 0
src/main/angular/src/models/search-result.model.ts

@@ -0,0 +1,34 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2016 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+
+import Person from './person.model';
+
+export default class SearchResult {
+    sizeExceeded: boolean;
+    people: Person[];
+
+    constructor(options: any) {
+        this.sizeExceeded = options.sizeExceeded;
+        this.people = options.searchResults.map((person: any) => new Person(person));
+    }
+}

+ 17 - 5
src/main/angular/src/peoplesearch/orgchart-search.component.html

@@ -1,16 +1,28 @@
 <mf-app-bar>
     <div class="page-content-title" translate="Title_Organization">Organization</div>
-    <mf-auto-complete flex
-                   search="$ctrl.autoCompleteSearch(query)"
-                   item-selected="$ctrl.onAutoCompleteItemSelected(person)"
-                   item="person">
+    <mf-auto-complete search-text="$ctrl.query"
+                      on-search-text-change="$ctrl.onSearchTextChange(value)"
+                      search="$ctrl.autoCompleteSearch(query)"
+                      item-selected="$ctrl.onAutoCompleteItemSelected(person)"
+                      item="person">
         <content-template>
-            <span ng-bind="person.displayName"></span>
+            <span ng-bind="person._displayName"></span>
         </content-template>
     </mf-auto-complete>
+    <span flex></span>
+    <mf-icon-button
+            icon="view-tile"
+            ng-click="$ctrl.gotoSearchState('search.cards')"
+            ng-attr-title="{{ 'Title_PeopleSearch' | translate }}"></mf-icon-button>
+    <mf-icon-button
+            icon="view-list"
+            ng-click="$ctrl.gotoSearchState('search.table')"
+            ng-attr-title="{{ 'Title_PeopleSearch' | translate }}"></mf-icon-button>
 </mf-app-bar>
 
 <org-chart person="$ctrl.person"
            direct-reports="$ctrl.directReports"
            management-chain="$ctrl.managementChain">
 </org-chart>
+
+<ui-view></ui-view>

+ 28 - 43
src/main/angular/src/peoplesearch/orgchart-search.component.scss

@@ -1,52 +1,37 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2016 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+
 org-chart-search {
   display: flex;
   flex-flow: column nowrap;
   height: 100%;
-  overflow-x: auto;
-
-  mf-app-bar {
-    > .mf-app-bar-content {
-      display: block;
-
-      > .page-content-title {
-        width: 100%;
-      }
 
-      > mf-auto-complete {
-        .results {
-          span {
-            color: #808080;
-            font-size: 13px;
-          }
-        }
-      }
-    }
+  > org-chart {
+    flex: 1 1;
+    overflow-x: auto;
   }
-}
 
-@media (min-width: 426px) {
-  org-chart-search {
-    mf-app-bar {
-      > .mf-app-bar-content {
-        display: flex;
-
-        > .page-content-title {
-          width: auto;
-        }
-
-        > mf-auto-complete {
-          max-width: 360px;
-
-          > mf-search-bar {
-            max-width: 100%;
-          }
-
-          > .results {
-            min-width: 100%;
-            width: auto;
-          }
-        }
-      }
-    }
+  mf-app-bar {
+    margin-bottom: 10px;
   }
 }

+ 48 - 6
src/main/angular/src/peoplesearch/orgchart-search.component.ts

@@ -1,5 +1,28 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2016 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+
 import { Component } from '../component';
-import { IPromise, IQService, IScope } from 'angular';
+import { isArray, isString, IPromise, IQService, IScope } from 'angular';
 import { IPeopleService } from '../services/people.service';
 import Person from '../models/person.model';
 import OrgChartData from '../models/orgchart-data.model';
@@ -12,6 +35,7 @@ export default class OrgChartSearchComponent {
     directReports: Person[];
     managementChain: Person[];
     person: Person;
+    query: string;
 
     static $inject = [ '$q', '$scope', '$state', '$stateParams', 'PeopleService' ];
     constructor(private $q: IQService,
@@ -22,9 +46,19 @@ export default class OrgChartSearchComponent {
     }
 
     $onInit(): void {
-        var self = this;
+        const self = this;
 
-        var personId: string = this.$stateParams['personId'];
+        // Read query from state parameters
+        const queryParameter = this.$stateParams['query'];
+        // If multiple query parameters are defined, use the first one
+        if (isArray(queryParameter)) {
+            this.query = queryParameter[0].trim();
+        }
+        else if (isString(queryParameter)) {
+            this.query = queryParameter.trim();
+        }
+
+        let personId: string = this.$stateParams['personId'];
 
         this.fetchOrgChartData(personId)
             .then((orgChartData: OrgChartData) => {
@@ -43,8 +77,8 @@ export default class OrgChartSearchComponent {
                         self.person = data['person'];
                     });
                 })
-                .catch((result) => {
-                    console.log(result);
+                .catch(() => {
+                    // TODO: error handling
                 });
             });
     }
@@ -53,8 +87,16 @@ export default class OrgChartSearchComponent {
         return this.peopleService.autoComplete(query);
     }
 
+    gotoSearchState(state: string) {
+        this.$state.go(state, { query: this.query });
+    }
+
     onAutoCompleteItemSelected(person: Person): void {
-        this.$state.go('orgchart', { personId: person.userKey });
+        this.$state.go('orgchart.search', { personId: person.userKey, query: null });
+    }
+
+    onSearchTextChange(value: string): void {
+        this.query = value;
     }
 
     private fetchOrgChartData(personId): IPromise<OrgChartData> {

+ 8 - 3
src/main/angular/src/peoplesearch/orgchart.component.html

@@ -8,7 +8,7 @@
             <div class="org-chart-connector"></div>
             <person-card person="manager"
                          size="{{ $ctrl.getManagerCardSize() }}"
-                         show-direct-report-count="true"
+                         show-direct-report-count="false"
                          ng-click="$ctrl.selectPerson(manager.userKey)">
             </person-card>
         </div>
@@ -22,7 +22,11 @@
 </div>
 
 <div class="org-chart-section">
-    <person-card person="$ctrl.person" direct-reports="$ctrl.directReports" size="large" show-direct-report-count="true"></person-card>
+    <person-card person="$ctrl.person"
+                 direct-reports="$ctrl.directReports"
+                 ng-click="$ctrl.onClickPerson()"
+                 size="large"
+                 show-direct-report-count="true"></person-card>
 </div>
 
 <div class="org-chart-section direct-reports" ng-if="$ctrl.hasDirectReports()">
@@ -30,7 +34,8 @@
     <div class="org-chart-connector"></div>
 
     <div class="person-card-list">
-        <person-card person="directReport" show-direct-report-count="true"
+        <person-card person="directReport"
+                     show-direct-report-count="false"
                      ng-repeat="directReport in $ctrl.directReports"
                      ng-click="$ctrl.selectPerson(directReport.userKey)">
         </person-card>

+ 36 - 13
src/main/angular/src/peoplesearch/orgchart.component.scss

@@ -1,3 +1,26 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2016 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+
 $org-chart-connector-color: #808080;
 $org-chart-secondary-connector-color: #dae1e1;
 $org-chart-text-color: #808080;
@@ -7,8 +30,16 @@ $manager-connector-height: 16px;
 // (XS) Default display
 org-chart {
   display: block;
-  min-width: 300px;
-  padding-top: 10px;
+  max-width: 100%;
+
+  > .org-chart-section {
+    width: 100%;
+
+    > person-card {
+      &[size="large"] {
+      }
+    }
+  }
 
   // (S) Too wide for full width person-card in direct reports
   &.small {
@@ -16,7 +47,7 @@ org-chart {
       > .person-card-list {
         > person-card {
           margin-right: 5px;
-          width: 200px;
+          width: 220px;
         }
       }
     }
@@ -25,10 +56,6 @@ org-chart {
   // (M) Wide enough to fit multiple person-cards next to each other inline
   &.medium {
     > .org-chart-section {
-      > .person-card-list {
-        text-align: left;
-      }
-
       &.managers {
         .manager {
           text-align: center;
@@ -49,7 +76,7 @@ org-chart {
       }
 
       .org-chart-connector {
-        left: 169px;
+        left: 172px;
         margin: 0;
       }
 
@@ -136,10 +163,6 @@ org-chart {
           }
         }
       }
-
-      .org-chart-connector {
-        left: 172px;
-      }
     }
   }
 
@@ -185,7 +208,7 @@ org-chart {
 
             > .person-card-content {
               > .avatar {
-                background: $org-chart-secondary-connector-color url('../../images/icons/m_question_mark.svg');
+                background: $org-chart-secondary-connector-color;
                 border-color: $org-chart-secondary-connector-color;
               }
             }

+ 22 - 2
src/main/angular/src/peoplesearch/orgchart.component.test.ts

@@ -1,5 +1,25 @@
-// import 'jasmine';
-// import { OrgChartComponent } from './orgchart.component';
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2016 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
 
 describe('testing OrgChartComponent', () => {
     beforeEach(() => {

+ 47 - 75
src/main/angular/src/peoplesearch/orgchart.component.ts

@@ -1,12 +1,36 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2016 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+
 import { Component } from '../component';
 import { element, IAugmentedJQuery, IFilterService, IScope, IWindowService } from 'angular';
+import ElementSizeService from '../ux/element-size.service';
 import Person from '../models/person.model';
 
 export enum OrgChartSize {
     ExtraSmall = 0,
     Small = 365,
     Medium = 410,
-    Large = 454,
+    Large = 450,
     ExtraLarge = 480
 }
 
@@ -22,6 +46,7 @@ export enum OrgChartSize {
 export default class OrgChartComponent {
     directReports: Person[];
     elementWidth: number;
+    isExtraLargeLayout: boolean;
     managementChain: Person[];
     person: Person;
 
@@ -37,33 +62,26 @@ export default class OrgChartComponent {
     private maxVisibleManagers: number;
     private visibleManagers: Person[];
 
-    static $inject = [ '$element', '$filter', '$scope', '$state', '$window' ];
+    static $inject = [ '$element', '$filter', '$scope', '$state', '$window', 'MfElementSizeService' ];
     constructor(
         private $element: IAugmentedJQuery,
         private $filter: IFilterService,
         private $scope: IScope,
         private $state: angular.ui.IStateService,
-        private $window: IWindowService) {
+        private $window: IWindowService,
+        private elementSizeService: ElementSizeService) {
     }
 
     $onDestroy(): void {
-        element(this.$window).off();
+        // TODO: remove $window click listener
     }
 
     $onInit(): void {
-        var self = this;
-
-        this.updateLayout();
-
         // OrgChartComponent has different functionality at different widths. On element resize, we
         // want to update the state of the component and trigger a $digest
-        element(this.$window).on('resize', () => {
-            self.elementWidth = self.getElementWidth();
-            self.$scope.$apply();
-        });
-        this.$scope.$watch('$ctrl.elementWidth', () => {
-            self.updateLayout();
-        });
+        this.elementSizeService
+            .watchWidth(this.$element, OrgChartSize)
+            .onResize(this.onResize.bind(this));
 
         // In large displays managers are displayed in a row. Any time this property changes, we want
         // to force our manager list to be recalculated in this.getManagementChain() so it returns the correct
@@ -74,12 +92,12 @@ export default class OrgChartComponent {
     }
 
     getManagerCardSize(): string {
-        return this.isExtraLargeLayout() ? 'small' : 'normal';
+        return this.isExtraLargeLayout ? 'small' : 'normal';
     }
 
     getManagementChain(): Person[] {
         // Display managers in a row
-        if (this.isExtraLargeLayout()) {
+        if (this.isExtraLargeLayout) {
             // All managers can fit on screen
             if (this.maxVisibleManagers >= this.managementChain.length) {
                 return this.managementChain;
@@ -90,7 +108,7 @@ export default class OrgChartComponent {
                 // Show a blank manager as last manager in the chain in place of
                 // the last visible manager. Blank manager links to the new last visible manager.
                 this.visibleManagers = this.managementChain.slice(0, this.maxVisibleManagers - 1);
-                var lastManager = this.managementChain[this.maxVisibleManagers - 2];
+                const lastManager = this.managementChain[this.maxVisibleManagers - 2];
 
                 this.visibleManagers.push(new Person({
                     userKey: lastManager.userKey,
@@ -118,8 +136,14 @@ export default class OrgChartComponent {
         return !(this.hasDirectReports() || this.hasManagementChain());
     }
 
+    onClickPerson(): void {
+        if (this.person) {
+            this.$state.go('orgchart.search.details', { personId: this.person.userKey });
+        }
+    }
+
     selectPerson(userKey: string): void {
-        this.$state.go('orgchart', { personId: userKey });
+        this.$state.go('orgchart.search', { personId: userKey });
     }
 
     showingOverflow(): boolean {
@@ -128,67 +152,15 @@ export default class OrgChartComponent {
             this.visibleManagers.length < this.managementChain.length;
     }
 
-    private getElementWidth() {
-        return this.$element[0].clientWidth;
-    }
+    private onResize(newValue: number): void {
+        this.isExtraLargeLayout = (newValue >= OrgChartSize.ExtraLarge);
 
-    private isExtraLargeLayout(): boolean {
-        return this.elementSize === OrgChartSize.ExtraLarge;
+        this.maxVisibleManagers = Math.floor(
+         (newValue - 115 /* left margin */) / 125 /* card width + right margin */);
     }
 
     // Remove all displayed managers so the list is updated on element resize
     private resetManagerList(): void {
         this.visibleManagers = null;
     }
-
-    private setElementClass(): void {
-        var className: string = [
-            OrgChartSize.Small,
-            OrgChartSize.ExtraSmall,
-            OrgChartSize.Medium,
-            OrgChartSize.Large,
-            OrgChartSize.ExtraLarge
-        ]
-            .filter((size: OrgChartSize): boolean => {
-                return size <= this.elementSize;
-            })
-            .map((size: OrgChartSize): string => {
-                return this.$filter<(input: string) => string>('dasherize')(OrgChartSize[size]);
-            })
-            .join(' ');
-
-        this.$element[0].className = '';
-        this.$element.addClass(className);
-    }
-
-    private setElementSize(): void {
-        var elementWidth: number = this.getElementWidth();
-
-        if (elementWidth < OrgChartSize.Small) {
-            this.elementSize = OrgChartSize.ExtraSmall;
-        }
-        else if (elementWidth < OrgChartSize.Medium) {
-            this.elementSize = OrgChartSize.Small;
-        }
-        else if (elementWidth < OrgChartSize.Large) {
-            this.elementSize = OrgChartSize.Medium;
-        }
-        else if (elementWidth < OrgChartSize.ExtraLarge) {
-            this.elementSize = OrgChartSize.Large;
-        }
-        else {
-            this.elementSize = OrgChartSize.ExtraLarge;
-        }
-    }
-
-    private setMaxVisibleManagers(): void {
-        this.maxVisibleManagers = Math.floor(
-            (this.getElementWidth() - 115 /* left margin */) / 125 /* card width + right margin */);
-    }
-
-    private updateLayout(): void {
-        this.setElementSize();
-        this.setElementClass();
-        this.setMaxVisibleManagers();
-    }
 }

+ 137 - 14
src/main/angular/src/peoplesearch/peoplesearch-base.component.ts

@@ -1,27 +1,150 @@
-import { IScope } from 'angular';
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2016 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+
+import { IPeopleService } from '../services/people.service';
+import { isArray, isString, IPromise, IScope } from 'angular';
 import Person from '../models/person.model';
-import {IPeopleService} from '../services/people.service';
+import SearchResult from '../models/search-result.model';
 
-declare var PWM_PS: any;
+interface ISearchFunction {
+    (query: string): IPromise<SearchResult>;
+}
 
 export default class PeopleSearchBaseComponent {
-    people: Person[];
+    loading: boolean;
     query: string;
+    searchFunction: ISearchFunction;
+    searchMessage: (string | IPromise<string>);
+    searchResult: SearchResult;
 
-    constructor(protected $scope: IScope,
-                protected $state: angular.ui.IStateService,
-                protected $stateParams: angular.ui.IStateParamsService,
-                protected peopleService: IPeopleService) {}
+    protected constructor(protected $scope: IScope,
+                          protected $state: angular.ui.IStateService,
+                          protected $stateParams: angular.ui.IStateParamsService,
+                          protected $translate: angular.translate.ITranslateService,
+                          protected peopleService: IPeopleService) {}
 
-    $onInit(): void {
-        this.query = this.$stateParams['query'];
+    gotoOrgchart(): void {
+        this.gotoState('orgchart.index');
     }
 
-    selectPerson(person: Person) {
-        PWM_PS.showUserDetail(person.userKey);
+    gotoState(state: string): void {
+        this.$state.go(state, { query: this.query });
     }
 
-    gotoState(state: string) {
-        this.$state.go(state, { query: this.query });
+    initialize(searchFunction: ISearchFunction): void {
+        this.searchFunction = searchFunction;
+
+        // Read query from state parameters
+        var queryParameter = this.$stateParams['query'];
+        // If multiple query parameters are defined, use the first one
+        if (isArray(queryParameter)) {
+            this.query = queryParameter[0].trim();
+        }
+        else if (isString(queryParameter)) {
+            this.query = queryParameter.trim();
+        }
+
+        this.fetchData();
+    }
+
+    onSearchBoxKeyDown(event: KeyboardEvent): void {
+        switch (event.keyCode) {
+            case 27: // ESC
+                this.clearSearch();
+                break;
+        }
+    }
+
+    onSearchTextChange(value: string): void {
+        if (value === this.query) {
+            return;
+        }
+
+        this.query = value;
+        this.setSearchMessage(null);
+        this.fetchData();
+    }
+
+    selectPerson(person: Person): void {
+        this.$state.go('.details', { personId: person.userKey, query: this.query });
+    }
+
+    protected setSearchMessage(message: (string | IPromise<string>)) {
+        if (!message) {
+            this.clearSearchMessage();
+            return;
+        }
+
+        if (typeof message === 'string') {
+            this.searchMessage = message;
+        }
+        else {
+            var self = this;
+
+            message.then((translation: string) => {
+                self.searchMessage = translation;
+                // self.$scope.$apply();
+            });
+        }
+    }
+
+    protected fetchData(): void {
+        const self = this;
+
+        if (!this.query) {
+            this.clearSearch();
+            return;
+        }
+
+        this.loading = true;
+
+        this.searchFunction
+            .call(this.peopleService, this.query)
+            .then((searchResult: SearchResult) => {
+                self.searchResult = searchResult;
+                self.clearSearchMessage();
+
+                // Too many results returned
+                if (searchResult.sizeExceeded) {
+                    self.setSearchMessage(self.$translate('Display_SearchResultsExceeded'));
+                }
+                // No results returned. Not an else if statement so that the more important message is presented
+                if (!searchResult.people.length) {
+                    self.setSearchMessage(self.$translate('Display_SearchResultsNone'));
+                }
+            })
+            .finally(() => {
+                self.loading = false;
+            });
+    }
+
+    private clearSearch(): void {
+        this.query = null;
+        this.searchResult = null;
+        this.clearSearchMessage();
+    }
+
+    private clearSearchMessage(): void  {
+        this.searchMessage = null;
     }
 }

+ 24 - 7
src/main/angular/src/peoplesearch/peoplesearch-cards.component.html

@@ -1,14 +1,31 @@
 <mf-app-bar>
     <div class="page-content-title" translate="Title_PeopleSearch">People Search</div>
+    <mf-search-bar search-text="$ctrl.query"
+                   on-search-text-change="$ctrl.onSearchTextChange(value)"
+                   on-key-down="$ctrl.onSearchBoxKeyDown($event)"
+                   auto-focus></mf-search-bar>
     <span flex></span>
-    <mf-icon-button icon="table-large" ng-click="$ctrl.gotoTableView()"></mf-icon-button>
+    <mf-icon-button
+            icon="view-list"
+            ng-click="$ctrl.gotoTableView()"
+            ng-attr-title="{{ 'Title_PeopleSearch' | translate }}"></mf-icon-button>
+    <mf-icon-button
+            icon="orgchart"
+            ng-click="$ctrl.gotoOrgchart()"
+            ng-attr-title="{{ 'Title_OrgChart' | translate }}"></mf-icon-button>
 </mf-app-bar>
 
-<mf-search-bar search-text="$ctrl.query" auto-focus></mf-search-bar>
+<div class="people-search-component-content">
+    <div class="search-info"
+         ng-if="$ctrl.loading || $ctrl.searchMessage"
+         ng-bind="$ctrl.loading ? ('Display_PleaseWait' | translate) : $ctrl.searchMessage"></div>
 
-<div class="person-card-list">
-    <person-card person="person"
-                 ng-repeat="person in $ctrl.people"
-                 ng-click="$ctrl.selectPerson(person)">
-    </person-card>
+    <div class="person-card-list">
+        <person-card person="person"
+                     ng-repeat="person in $ctrl.searchResult.people"
+                     ng-click="$ctrl.selectPerson(person)">
+        </person-card>
+    </div>
+
+    <ui-view></ui-view>
 </div>

+ 61 - 23
src/main/angular/src/peoplesearch/peoplesearch-cards.component.scss

@@ -1,32 +1,70 @@
-people-search-cards {
-    display: flex;
-    flex-flow: column nowrap;
-    height: 100%;
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2016 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
 
-    > .person-card-list {
-        height: 100%;
-        overflow: auto;
-        text-align: center;
+people-search-cards {
+  display: flex;
+  flex-flow: column nowrap;
+  height: 100%;
 
+  // At medium size, cards are centered and no longer take up 100% width
+  &.medium {
+    > .people-search-component-content {
+      > .person-card-list {
         > person-card {
-            display: inline-block;
-            width: 100%;
+          margin: 0 auto;
+          display: block;
+          width: 220px;
+        }
+      }
+    }
+  }
 
-            &:not(:last-child) {
-                margin-bottom: 5px;
-            }
+  // At large size, cards fit next to each other
+  &.large {
+    > .people-search-component-content {
+      > .person-card-list {
+        text-align: left;margin: 0;
+        > person-card {
+          display: inline-block;
+          margin-right: 5px;
         }
+      }
     }
-}
+  }
+
+  > .people-search-component-content {
+    flex: 1 1;
+    overflow: auto;
+    text-align: center;
 
-@media (min-width: 375px) {
-    people-search-cards {
-        > .person-card-list {
-            text-align: left;
-            > person-card {
-                margin-right: 5px;
-                width: 200px;
-            }
+    > .person-card-list {
+      > person-card {
+        display: inline-block;
+        width: 100%;
+
+        &:not(:last-child) {
+          margin-bottom: 5px;
         }
+      }
     }
-}
+  }
+}

+ 51 - 25
src/main/angular/src/peoplesearch/peoplesearch-cards.component.ts

@@ -1,46 +1,72 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2016 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+
 import { Component } from '../component';
 import IPeopleService from '../services/people.service';
-import { IScope } from 'angular';
-import Person from '../models/person.model';
 import PeopleSearchBaseComponent from './peoplesearch-base.component';
+import { IAugmentedJQuery, IScope } from 'angular';
+import ElementSizeService from '../ux/element-size.service';
 
+export enum PeopleSearchCardsSize {
+    Small = 0,
+    Medium = 365,
+    Large = 450
+}
 
 @Component({
     stylesheetUrl: require('peoplesearch/peoplesearch-cards.component.scss'),
     templateUrl: require('peoplesearch/peoplesearch-cards.component.html')
 })
 export default class PeopleSearchCardsComponent extends PeopleSearchBaseComponent {
-    columnConfiguration: any;
-
-    static $inject = [ '$scope', '$state', '$stateParams', 'PeopleService' ];
-    constructor($scope: IScope,
+    static $inject = [
+        '$element',
+        '$scope',
+        '$state',
+        '$stateParams',
+        '$translate',
+        'MfElementSizeService',
+        'PeopleService'
+    ];
+    constructor(private $element: IAugmentedJQuery,
+                $scope: IScope,
                 $state: angular.ui.IStateService,
                 $stateParams: angular.ui.IStateParamsService,
+                $translate: angular.translate.ITranslateService,
+                private elementSizeService: ElementSizeService,
                 peopleService: IPeopleService) {
-        super($scope, $state, $stateParams, peopleService);
+        super($scope, $state, $stateParams, $translate, peopleService);
     }
 
-    $onInit(): void {
-        super.$onInit();
-
-        var self = this;
+    $onDestroy(): void {
+        // TODO: remove $window click listener
+    }
 
-        // Fetch data when query changes
-        this.$scope.$watch('$ctrl.query', (newValue: string) => {
-            if (!newValue) {
-                self.people = [];
-            }
-            else {
-                this.peopleService
-                    .cardSearch(newValue)
-                    .then((people: Person[]) => {
-                        self.people = people;
-                    });
-            }
-        });
+    $onInit(): void {
+        this.initialize(this.peopleService.cardSearch);
+        this.elementSizeService.watchWidth(this.$element, PeopleSearchCardsSize);
     }
 
     gotoTableView() {
-        super.gotoState('search.table');
+        this.gotoState('search.table');
     }
 }

+ 34 - 27
src/main/angular/src/peoplesearch/peoplesearch-table.component.html

@@ -1,27 +1,34 @@
-<!--<mf-table class="table-striped" data="person in $ctrl.people">-->
-    <!--<mf-table-column label="First Name" value="person.givenName"></mf-table-column>-->
-    <!--<mf-table-column label="Last Name" value="person.sn"></mf-table-column>-->
-    <!--<mf-table-column label="Title" value="person.title"></mf-table-column>-->
-    <!--<mf-table-column label="Email" value="person.mail"></mf-table-column>-->
-    <!--<mf-table-column label="Telephone" value="person.telephoneNumber"></mf-table-column>-->
-<!--</mf-table>-->
-<table st-table="rowCollection" ng-if="$ctrl.people.length" class="table table-striped">
-    <thead>
-        <tr>
-            <th>{{ 'COLUMN_FIRST_NAME' | translate }}</th>
-            <th>{{ 'COLUMN_LAST_NAME' | translate }}</th>
-            <th>{{ 'COLUMN_TITLE' | translate }}</th>
-            <th>{{ 'COLUMN_EMAIL' | translate }}</th>
-            <th>{{ 'COLUMN_TELEPHONE' | translate }}</th>
-        </tr>
-    </thead>
-    <tbody>
-        <tr ng-repeat="(key,value) in $ctrl.people.fields" ng-click="$ctrl.selectPerson(person.userKey)">
-            <td ng-repeat="(key,value) in $ctrl.people.fields">{{ value }}</td>
-            <td>{{ person.sn }}</td>
-            <td>{{ person.title }}</td>
-            <td>{{ person.mail }}</td>
-            <td>{{ person.telephoneNumber }}</td>
-        </tr>
-    </tbody>
-</table>
+<mf-app-bar>
+    <div class="page-content-title" translate="Title_PeopleSearch">People Search</div>
+    <mf-search-bar search-text="$ctrl.query"
+                   on-search-text-change="$ctrl.onSearchTextChange(value)"
+                   on-key-down="$ctrl.onSearchBoxKeyDown($event)"
+                   auto-focus></mf-search-bar>
+    <span flex></span>
+    <mf-icon-button
+            icon="view-tile"
+            ng-click="$ctrl.gotoCardsView()"
+            ng-attr-title="{{ 'Title_PeopleSearch' | translate }}"></mf-icon-button>
+    <mf-icon-button
+            icon="orgchart"
+            ng-click="$ctrl.gotoOrgchart()"
+            ng-attr-title="{{ 'Title_OrgChart' | translate }}"></mf-icon-button>
+</mf-app-bar>
+
+<div class="people-search-component-content">
+    <div class="search-info"
+         ng-if="$ctrl.loading || $ctrl.searchMessage"
+         ng-bind="$ctrl.loading ? ('Display_PleaseWait' | translate) : $ctrl.searchMessage"></div>
+
+    <mf-table data="person in $ctrl.searchResult.people"
+              ng-show="$ctrl.searchResult.people.length"
+              search-highlight="$ctrl.query"
+              on-click-item="$ctrl.selectPerson(person)">
+        <mf-table-column ng-repeat="(key, value) in $ctrl.columnConfiguration"
+                         label="value"
+                         value="'person.' + key">
+        </mf-table-column>
+    </mf-table>
+
+    <ui-view></ui-view>
+</div>

+ 25 - 2
src/main/angular/src/peoplesearch/peoplesearch-table.component.scss

@@ -1,11 +1,34 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2016 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+
 people-search-table {
     display: flex;
     flex-flow: column nowrap;
     height: 100%;
 
-    > mf-table {
+    > .people-search-component-content {
         flex: 1 1;
-        margin-top: 10px;
         overflow: auto;
+        text-align: center;
     }
 }

+ 31 - 23
src/main/angular/src/peoplesearch/peoplesearch-table.component.ts

@@ -1,10 +1,31 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2016 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+
 import { Component } from '../component';
+import { IConfigService } from '../services/config.service';
 import IPeopleService from '../services/people.service';
-import { IScope } from 'angular';
-import Person from '../models/person.model';
 import PeopleSearchBaseComponent from './peoplesearch-base.component';
-import {IConfigService} from '../services/config.service';
-
+import { IScope } from 'angular';
 
 @Component({
     stylesheetUrl: require('peoplesearch/peoplesearch-table.component.scss'),
@@ -13,41 +34,28 @@ import {IConfigService} from '../services/config.service';
 export default class PeopleSearchTableComponent extends PeopleSearchBaseComponent {
     columnConfiguration: any;
 
-    static $inject = [ '$scope', '$state', '$stateParams', 'ConfigService', 'PeopleService' ];
+    static $inject = [ '$scope', '$state', '$stateParams', '$translate', 'ConfigService', 'PeopleService' ];
     constructor($scope: IScope,
                 $state: angular.ui.IStateService,
                 $stateParams: angular.ui.IStateParamsService,
+                $translate: angular.translate.ITranslateService,
                 private configService: IConfigService,
                 peopleService: IPeopleService) {
-        super($scope, $state, $stateParams, peopleService);
+        super($scope, $state, $stateParams, $translate, peopleService);
     }
 
     $onInit(): void {
-        super.$onInit();
+        this.initialize(this.peopleService.search);
 
-        var self = this;
+        let self = this;
 
         // The table columns are dynamic and configured via a service
         this.configService.getColumnConfiguration().then((columnConfiguration: any) => {
             self.columnConfiguration = columnConfiguration;
         });
-
-        // Fetch data when query changes
-        this.$scope.$watch('$ctrl.query', (newValue: string) => {
-            if (!newValue) {
-                self.people = [];
-            }
-            else {
-                this.peopleService
-                    .search(newValue)
-                    .then((people: Person[]) => {
-                        self.people = people;
-                    });
-            }
-        });
     }
 
     gotoCardsView() {
-        super.gotoState('search.cards');
+        this.gotoState('search.cards');
     }
 }

+ 28 - 3
src/main/angular/src/peoplesearch/peoplesearch.module.ts

@@ -1,11 +1,35 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2016 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+
 import { module } from 'angular';
-import { DasherizeFilter } from './string.filters';
+import { HighlightFilter } from './string.filters';
 import { FullNameFilter } from './person.filters';
 import OrgChartComponent from './orgchart.component';
 import OrgChartSearchComponent from './orgchart-search.component';
 import PeopleSearchTableComponent from './peoplesearch-table.component';
 import PeopleSearchCardsComponent from './peoplesearch-cards.component';
 import PersonCardComponent from './person-card.component';
+import PersonDetailsDialogComponent from './person-details-dialog.component';
 import uxModule from '../ux/ux.module';
 
 require('./peoplesearch.scss');
@@ -16,12 +40,13 @@ module(moduleName, [
     'pascalprecht.translate',
     uxModule
 ])
-    .filter('dasherize', DasherizeFilter)
     .filter('fullName', FullNameFilter)
+    .filter('highlight', HighlightFilter)
     .component('orgChart', OrgChartComponent)
     .component('orgChartSearch', OrgChartSearchComponent)
     .component('personCard', PersonCardComponent)
     .component('peopleSearchTable', PeopleSearchTableComponent)
-    .component('peopleSearchCards', PeopleSearchCardsComponent);
+    .component('peopleSearchCards', PeopleSearchCardsComponent)
+    .component('personDetailsDialogComponent', PersonDetailsDialogComponent);
 
 export default moduleName;

+ 38 - 8
src/main/angular/src/peoplesearch/peoplesearch.scss

@@ -1,3 +1,26 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2016 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+
 body, html {
   height: 100%;
 }
@@ -23,15 +46,22 @@ body {
     overflow: auto;
   }
 
-  mf-search-bar {
-    margin: 10px 0;
+  mf-app-bar {
+    margin-bottom: 10px;
   }
-}
 
-@media (min-width: 454px) {
-  .people-search-component {
-    mf-search-bar {
-      width: 444px;
-    }
+  .search-info {
+    border: 1px solid #dae1e1;
+    border-radius: 3px;
+    color: #808080;
+    display: inline-block;
+    font-size: 14px;
+    margin: 0 auto 10px;
+    padding: 5px;
+    text-align: center;
   }
+}
+
+.highlight {
+  color: #28a9e1;
 }

+ 3 - 2
src/main/angular/src/peoplesearch/person-card.component.html

@@ -1,8 +1,9 @@
 <div class="person-card-content" ng-switch="$ctrl.size">
     <div class="avatar" ng-style="$ctrl.getAvatarStyle()" aria-label="User avatar"></div>
     <div class="reports"
-         ng-if="$ctrl.showDirectReportCount && $ctrl.person.numOfDirectReports"
-         ng-bind="$ctrl.person.numOfDirectReports"></div>
+         ng-if="$ctrl.showDirectReportCount && $ctrl.person.numDirectReports"
+         ng-bind="$ctrl.person.numDirectReports"
+         ng-attr-title="{{$ctrl.person.numDirectReports}} {{ 'Title_DirectReports' | translate }}"></div>
 
     <div class="details" ng-switch-when="small">
         <div ng-bind="$ctrl.person.displayNames[0]"></div>

+ 50 - 11
src/main/angular/src/peoplesearch/person-card.component.scss

@@ -1,3 +1,26 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2016 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+
 $text-color: #000000;
 $text-color-subtext: #808080;
 
@@ -5,7 +28,7 @@ $person-card-bg-color: #eef2f2;
 $person-card-hover-bg-color: #f6f9f8;
 $person-card-border-color: #28a9e1;
 $person-card-height: 82px;
-$person-card-width: 200px;
+$person-card-width: 220px;
 $person-card-avatar-size: 50px;
 $person-card-spacing: 10px;
 
@@ -16,7 +39,6 @@ person-card {
   border: 1px solid $person-card-bg-color;
   border-radius: 3px;
   box-sizing: border-box;
-  cursor: pointer;
   display: block;
   height: $person-card-height;
   padding: $person-card-spacing;
@@ -25,9 +47,15 @@ person-card {
   vertical-align: top;
   width: $person-card-width;
 
-  &:hover {
-    background-color: $person-card-hover-bg-color;
-    border-color: $person-card-border-color;
+  &[ng-click] {
+    cursor: pointer;
+
+    &:focus,
+    &:hover {
+      background-color: $person-card-hover-bg-color;
+      border-color: $person-card-border-color;
+      outline: none;
+    }
   }
 
   &[size="large"] {
@@ -35,12 +63,11 @@ person-card {
     border: 3px solid #808080;
     border-radius: 3px;
     height: 166px;
-    width: 326px;
+    width: 316px;
     max-width: 100%;
-    min-width: 310px;
 
     > .person-card-content {
-      flex-flow: row wrap;
+      flex-flow: row nowrap;
 
       > .avatar {
         flex: 0 0 $person-card-large-avatar-size;
@@ -62,6 +89,19 @@ person-card {
     padding: 0;
     width: 120px;
 
+    &[ng-click] {
+      &:focus,
+      &:hover {
+        background-color: transparent;
+
+        > .person-card-content {
+          > .avatar {
+            border-color: $person-card-border-color !important;
+          }
+        }
+      }
+    }
+
     > .person-card-content {
       display: block;
       text-align: center;
@@ -111,7 +151,6 @@ person-card {
       flex: 0 0 $person-card-avatar-size;
       height: $person-card-avatar-size;
       margin-right: $person-card-spacing;
-      width: $person-card-avatar-size;
 
       img {
         height: 100%;
@@ -120,8 +159,8 @@ person-card {
     }
 
     > .details {
-      flex-grow: 1;
-      width: $person-card-width - $person-card-avatar-size;
+      flex: 1;
+      overflow: hidden;
 
       > div {
         line-height: 16px;

+ 57 - 12
src/main/angular/src/peoplesearch/person-card.component.ts

@@ -1,3 +1,27 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2016 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+
+import { IAugmentedJQuery } from 'angular';
 import { Component } from '../component';
 import Person from '../models/person.model';
 import { IPeopleService } from '../services/people.service';
@@ -5,43 +29,55 @@ import { IPeopleService } from '../services/people.service';
 @Component({
     bindings: {
         directReports: '<',
+        disableFocus: '<',
         person: '<',
         size: '@',
-        showDirectReportCount: '@'
+        showDirectReportCount: '<'
     },
     stylesheetUrl: require('peoplesearch/person-card.component.scss'),
     templateUrl: require('peoplesearch/person-card.component.html')
 })
 export default class PersonCardComponent {
     private details: any[]; // For large style cards
+    private disableFocus: boolean;
     private person: Person;
     private directReports: Person[];
     private size: string;
     private showDirectReportCount: boolean;
 
-    static $inject = ['PeopleService'];
-    constructor(private peopleService: IPeopleService) {
+    static $inject = ['$element', 'PeopleService'];
+    constructor(private $element: IAugmentedJQuery, private peopleService: IPeopleService) {
         this.details = [];
+        this.size = 'medium';
     }
 
-    $onInit() {
+    $onInit(): void {
+        if (!this.disableFocus) {
+            this.$element[0].tabIndex = 0;
+            this.$element.on('keydown', this.onKeyDown.bind(this));
+        }
     }
 
-    $onChanges() {
+    $onChanges(): void {
         if (this.person) {
             this.setDisplayData();
 
             if (this.showDirectReportCount) {
                 this.peopleService.getNumberOfDirectReports(this.person.userKey)
-                    .then((numOfDirectReports) => {
-                        this.person.numOfDirectReports = numOfDirectReports;
-                    }).catch((result) => {
-                    console.log(result);
-                });
+                    .then((numDirectReports) => {
+                        this.person.numDirectReports = numDirectReports;
+                    })
+                    .catch(() => {
+                        // TODO: error handling
+                    });
             }
         }
     }
 
+    $onDestroy(): void {
+        this.$element.off('keydown', this.onKeyDown.bind(this));
+    }
+
     getAvatarStyle(): any {
         if (this.person && this.person.photoURL) {
             return { 'background-image': 'url(' + this.person.photoURL + ')' };
@@ -50,7 +86,16 @@ export default class PersonCardComponent {
         return {};
     }
 
-    private setDisplayData() {
+    private onKeyDown(event: KeyboardEvent): void {
+        if (event.keyCode === 13 || event.keyCode === 32) { // 13 = Enter, 32 = Space
+            this.$element.triggerHandler('click');
+
+            event.preventDefault();
+            event.stopImmediatePropagation();
+        }
+    }
+
+    private setDisplayData(): void {
         if (this.person.detail) {
             this.details = Object
                 .keys(this.person.detail)
@@ -60,7 +105,7 @@ export default class PersonCardComponent {
         }
 
         if (this.directReports) {
-            this.person.numOfDirectReports = this.directReports.length;
+            this.person.numDirectReports = this.directReports.length;
         }
     }
 }

+ 51 - 0
src/main/angular/src/peoplesearch/person-details-dialog.component.html

@@ -0,0 +1,51 @@
+<mf-dialog class="person-details" on-close="$ctrl.closeDialog()">
+    <mf-app-bar>
+        <div class="page-content-title" translate="Title_Details">Details</div>
+    </mf-app-bar>
+    <div class="mf-dialog-content">
+        <person-card size="medium"
+                     person="$ctrl.person"
+                     disable-focus="true"
+                     show-direct-report-count="false"></person-card>
+        <mf-button type="button" ng-click="$ctrl.gotoOrgChart()">
+            <mf-icon icon="orgchart"></mf-icon>
+            <span translate="Title_OrgChart">Organizational Chart</span>
+        </mf-button>
+
+        <!-- Details -->
+        <table>
+            <tbody>
+                <tr ng-repeat="(key, detail) in $ctrl.person.detail">
+                    <td ng-bind="detail.label"></td>
+                    <td ng-switch="detail.type">
+                        <div class="detail-container" ng-switch-when="userDN">
+                            <ul>
+                                <li ng-repeat="user in detail.userReferences">
+                                    <a ng-href="{{$ctrl.getPersonDetailsUrl(user.userKey)}}"
+                                       ng-bind="user.displayName"></a>
+                                </li>
+                            </ul>
+                        </div>
+                        <div class="detail-container" ng-switch-default>
+                            <ul>
+                                <li ng-repeat="value in detail.values">
+                                    <a ng-href="mailto:{{value}}"
+                                       ng-bind="value"
+                                       ng-if="detail.type === 'email'"
+                                       flex></a>
+                                    <span ng-bind="value"
+                                          ng-if="detail.type !== 'email'"
+                                          flex></span>
+                                    <mf-icon-button icon="magnify"
+                                                    ng-click="$ctrl.searchText(value)"
+                                                    ng-if="detail.searchable"
+                                                    ng-attr-title="{{'Search \'' + value + '\''}}"></mf-icon-button>
+                                </li>
+                            </ul>
+                        </div>
+                    </td>
+                </tr>
+            </tbody>
+        </table>
+    </div>
+</mf-dialog>

+ 95 - 0
src/main/angular/src/peoplesearch/person-details-dialog.component.scss

@@ -0,0 +1,95 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2016 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+
+.person-details {
+  mf-app-bar {
+    padding: 0 5px;
+  }
+
+  .mf-dialog-content {
+    > table {
+      border: 1px solid #dae1e1;
+      border-collapse: collapse;
+      //box-sizing: border-box;
+      width: 100%;
+
+      tr {
+        height: 25px;
+
+        td {
+          border: 1px solid #dae1e1;
+          font-size: 12px;
+          height: 19px;
+          padding: 3px 5px;
+          text-align: left;
+
+          &:first-child {
+            color: #949494;
+            text-align: right;
+            padding: 3px 15px;
+          }
+
+          &:last-child {
+            > .detail-container {
+              a {
+                cursor: pointer;
+                text-decoration: underline;
+              }
+
+              > ul {
+                list-style: none;
+                margin: 0;
+                padding: 0;
+
+                > li {
+                  display: flex;
+                  flex-direction: row;
+                  margin: 0;
+                  padding: 0;
+
+                  > [flex] {
+                    flex: 1 1;
+                  }
+
+                  > mf-icon-button {
+                    display: inline-block;
+                    flex: 0 0 16px;
+                    height: 16px;
+                    width: 16px;
+
+                    > button {
+                      > mf-icon {
+                        font-size: 16px;
+                      }
+                    }
+                  }
+                }
+              }
+            }
+          }
+        }
+      }
+    }
+  }
+}
+

+ 76 - 0
src/main/angular/src/peoplesearch/person-details-dialog.component.ts

@@ -0,0 +1,76 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2016 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+
+import { Component } from '../component';
+import { IPeopleService } from '../services/people.service';
+import Person from '../models/person.model';
+import { IAugmentedJQuery, IScope, ITimeoutService } from 'angular';
+
+@Component({
+    stylesheetUrl: require('peoplesearch/person-details-dialog.component.scss'),
+    templateUrl: require('peoplesearch/person-details-dialog.component.html')
+})
+export default class PersonDetailsDialogComponent {
+    person: Person;
+
+    static $inject = [ '$element', '$state', '$stateParams', '$timeout', 'PeopleService' ];
+    constructor(private $element: IAugmentedJQuery,
+                private $state: angular.ui.IStateService,
+                private $stateParams: angular.ui.IStateParamsService,
+                private $timeout: ITimeoutService,
+                private peopleService: IPeopleService) {
+    }
+
+    $onInit(): void {
+        const personId = this.$stateParams['personId'];
+
+        this.peopleService
+            .getPerson(personId)
+            .then((person: Person) => {
+                this.person = person;
+            });
+    }
+
+    $postLink() {
+        const self = this;
+        this.$timeout(() => {
+            self.$element.find('button')[0].focus();
+        }, 100);
+    }
+
+    closeDialog(): void {
+        this.$state.go('^', { query: this.$stateParams['query'] });
+    }
+
+    gotoOrgChart(): void {
+        this.$state.go('orgchart.search', { personId: this.person.userKey });
+    }
+
+    getPersonDetailsUrl(personId: string): string {
+        return this.$state.href('.', { personId: personId }, { inherit: true, });
+    }
+
+    searchText(text: string): void {
+        this.$state.go('search.table', { query: text });
+    }
+}

+ 23 - 0
src/main/angular/src/peoplesearch/person.filters.ts

@@ -1,3 +1,26 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2016 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+
 import Person from '../models/person.model';
 
 export function FullNameFilter(): (person: Person) => string {

+ 34 - 7
src/main/angular/src/peoplesearch/string.filters.ts

@@ -1,9 +1,36 @@
-export function DasherizeFilter(): (input: string) => string {
-    return (input: string): string => {
-        return input
-            .replace(/(?:^\w|[A-Z]|\b\w)/g, function (letter, index) {
-                return (index == 0 ? '' : '-') + letter.toLowerCase();
-            })
-            .replace(/\s+/g, '');
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2016 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+
+export function HighlightFilter() {
+    return (input: string, searchText: string): string => {
+        if (!input || !searchText || !searchText.length) {
+            return input;
+        }
+
+        const searchTextRegExp = new RegExp(searchText, 'gi');
+
+        return input.replace(searchTextRegExp, function (match) {
+            return `<span class="highlight">${match}</span>`;
+        });
     };
 }

+ 39 - 3
src/main/angular/src/routes.ts

@@ -1,3 +1,26 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2016 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+
 export default [
     '$stateProvider',
     '$urlRouterProvider',
@@ -8,15 +31,28 @@ export default [
         $locationProvider: angular.ILocationProvider
     ) => {
         $urlRouterProvider.otherwise('/search/cards');
-        $locationProvider.html5Mode(true);
+        $locationProvider.html5Mode({
+            enabled: true,
+            requireBase: false
+        });
 
         $stateProvider.state('search', {
             url: '/search?query',
             abstract: true,
             template: '<div class="people-search-component"><ui-view/></div>',
-            reloadOnSearch: false
         });
         $stateProvider.state('search.table', { url: '/table', component: 'peopleSearchTable' });
         $stateProvider.state('search.cards', { url: '/cards', component: 'peopleSearchCards' });
-        $stateProvider.state('orgchart', { url: '/orgchart/{personId}', component: 'orgChartSearch' });
+        $stateProvider.state('search.table.details', {
+            url: '/details/{personId}',
+            component: 'personDetailsDialogComponent'
+        });
+        $stateProvider.state('search.cards.details', {
+            url: '/details/{personId}',
+            component: 'personDetailsDialogComponent'
+        });
+        $stateProvider.state('orgchart', { url: '/orgchart?query', abstract: true, template: '<ui-view/>' });
+        $stateProvider.state('orgchart.index', { url: '', component: 'orgChartSearch' });
+        $stateProvider.state('orgchart.search', { url: '/{personId}', component: 'orgChartSearch' });
+        $stateProvider.state('orgchart.search.details', { url: '/details', component: 'personDetailsDialogComponent' });
     }];

+ 26 - 6
src/main/angular/src/services/config.service.dev.ts

@@ -1,12 +1,32 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2016 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+
 import { IConfigService } from './config.service';
 import { IPromise, IQService } from 'angular';
-import PwmService from './pwm.service';
 
-export default class ConfigService extends PwmService implements IConfigService {
-    static $inject = ['$q'];
-    constructor(private $q: IQService) {
-        super();
-    }
+export default class ConfigService implements IConfigService {
+    static $inject = [ '$q' ];
+    constructor(private $q: IQService) {}
 
     getColumnConfiguration(): IPromise<any> {
         return this.$q.resolve({

+ 29 - 5
src/main/angular/src/services/config.service.ts

@@ -1,3 +1,26 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2016 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+
 import { IHttpService, IPromise, IQService } from 'angular';
 import PwmService from './pwm.service';
 
@@ -5,15 +28,16 @@ export interface IConfigService {
     getColumnConfiguration(): IPromise<any>;
 }
 
-export default class ConfigService extends PwmService implements IConfigService {
-    static $inject = ['$http', '$q'];
-    constructor(private $http: IHttpService, private $q: IQService) {
-        super();
+export default class ConfigService implements IConfigService {
+    static $inject = ['$http', '$q', 'PwmService' ];
+    constructor(private $http: IHttpService,
+                private $q: IQService,
+                private pwmService: PwmService) {
     }
 
     getColumnConfiguration(): IPromise<any> {
         return this.$http
-            .get(this.getServerUrl('clientData'))
+            .get(this.pwmService.getServerUrl('clientData'), { cache: true })
             .then((response) => {
                 return this.$q.resolve(response.data['data']['peoplesearch_search_columns']);
             });

File diff suppressed because it is too large
+ 202 - 145
src/main/angular/src/services/people.data.json


+ 73 - 19
src/main/angular/src/services/people.service.dev.ts

@@ -1,9 +1,34 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2016 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+
 import { IPromise, IQService } from 'angular';
 import Person from '../models/person.model';
 import { IPeopleService } from './people.service';
 import OrgChartData from '../models/orgchart-data.model';
+import SearchResult from '../models/search-result.model';
 
-var peopleData = require('./people.data');
+const peopleData = require('./people.data');
+const MAX_RESULTS = 10;
 
 export default class PeopleService implements IPeopleService {
     private people: Person[];
@@ -11,11 +36,37 @@ export default class PeopleService implements IPeopleService {
     static $inject = ['$q'];
     constructor(private $q: IQService) {
         this.people = peopleData.map((person) => new Person(person));
+
+        // Create directReports detail (instead of managing this in people.data.json
+        this.people.forEach((person: Person) => {
+            const directReports = this.findDirectReports(person.userKey);
+
+            if (!directReports.length) {
+                return;
+            }
+
+            person.detail.directReports = {
+                name: 'directReports',
+                label: 'Direct Reports',
+                type: 'userDN',
+                userReferences: directReports
+                    .map((directReport: Person) => {
+                        return {
+                            userKey: directReport.userKey,
+                            displayName: directReport._displayName
+                        };
+                    })
+            };
+        }, this);
     }
 
     autoComplete(query: string): IPromise<Person[]> {
         return this.search(query)
-            .then((people: Person[]) => {
+            .then((searchResult: SearchResult) => {
+                let people = searchResult.people;
+                // Alphabetize results by _displayName
+                people = people.sort((person1, person2) => person1._displayName.localeCompare(person2._displayName));
+
                 if (people && people.length > 10) {
                     return this.$q.resolve(people.slice(0, 10));
                 }
@@ -24,21 +75,21 @@ export default class PeopleService implements IPeopleService {
             });
     }
 
-    cardSearch(query: string): angular.IPromise<Person[]> {
+    cardSearch(query: string): angular.IPromise<SearchResult> {
         return this.search(query);
     }
 
     getDirectReports(id: string): angular.IPromise<Person[]> {
-        var people = this.findDirectReports(id);
+        const people = this.findDirectReports(id);
 
         return this.$q.resolve(people);
     }
 
     getManagementChain(id: string): angular.IPromise<Person[]> {
-        var person = this.findPerson(id);
+        let person = this.findPerson(id);
 
         if (person) {
-            var managementChain: Person[] = [];
+            const managementChain: Person[] = [];
 
             while (person = this.findManager(person)) {
                 managementChain.push(person);
@@ -55,11 +106,11 @@ export default class PeopleService implements IPeopleService {
             personId = '9';
         }
 
-        var self = this.findPerson(personId);
-        var manager = this.findManager(self);
-        var children = this.findDirectReports(personId);
+        const self = this.findPerson(personId);
+        const manager = this.findManager(self);
+        const children = this.findDirectReports(personId);
 
-        var orgChartData = new OrgChartData(manager, children, self);
+        const orgChartData = new OrgChartData(manager, children, self);
 
         return this.$q.resolve(orgChartData);
     }
@@ -72,7 +123,7 @@ export default class PeopleService implements IPeopleService {
     }
 
     getPerson(id: string): IPromise<Person> {
-        var person = this.findPerson(id);
+        const person = this.findPerson(id);
 
         if (person) {
             return this.$q.resolve(person);
@@ -85,29 +136,32 @@ export default class PeopleService implements IPeopleService {
         return this.$q.resolve(true);
     }
 
-    search(query: string): angular.IPromise<Person[]> {
-        var people = this.people.filter((person: Person) => {
+    search(query: string): angular.IPromise<SearchResult> {
+        let people = this.people.filter((person: Person) => {
             if (!query) {
                 return false;
             }
-            var fullName = `${person.givenName} ${person.sn}`;
-            return fullName.toLowerCase().indexOf(query.toLowerCase()) >= 0;
+            return person._displayName.toLowerCase().indexOf(query.toLowerCase()) >= 0;
         });
 
-        return this.$q.resolve(people);
+        const sizeExceeded = (people.length > MAX_RESULTS);
+        if (sizeExceeded) {
+            people = people.slice(MAX_RESULTS);
+        }
 
+        return this.$q.resolve(new SearchResult({sizeExceeded: sizeExceeded, searchResults: people}));
     }
 
     private findDirectReports(id: string): Person[] {
-        return this.people.filter((person: Person) => person.detail['manager']['typeMetaData'].userKey == id);
+        return this.people.filter((person: Person) => person.detail['manager']['userReferences'][0].userKey == id);
     }
 
     private findManager(person: Person): Person {
-        return this.findPerson(person.detail['manager']['typeMetaData'].userKey);
+        return this.findPerson(person.detail['manager']['userReferences'][0].userKey);
     }
 
     private findPerson(id: string): Person {
-        var people = this.people.filter((person: Person) => person.userKey == id);
+        const people = this.people.filter((person: Person) => person.userKey == id);
 
         if (people.length) {
             return people[0];

+ 68 - 36
src/main/angular/src/services/people.service.ts

@@ -1,29 +1,53 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2016 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+
 import { IHttpService, IPromise, IQService } from 'angular';
 import Person from '../models/person.model';
 import PwmService from './pwm.service';
 import OrgChartData from '../models/orgchart-data.model';
+import SearchResult from '../models/search-result.model';
 
 export interface IPeopleService {
     autoComplete(query: string): IPromise<Person[]>;
-    cardSearch(query: string): IPromise<Person[]>;
+    cardSearch(query: string): IPromise<SearchResult>;
     getDirectReports(personId: string): IPromise<Person[]>;
     getNumberOfDirectReports(personId: string): IPromise<number>;
     getManagementChain(personId: string): IPromise<Person[]>;
     getOrgChartData(personId: string): IPromise<OrgChartData>;
     getPerson(id: string): IPromise<Person>;
     isOrgChartEnabled(id: string): IPromise<boolean>;
-    search(query: string): IPromise<Person[]>;
+    search(query: string): IPromise<SearchResult>;
 }
 
-export default class PeopleService extends PwmService implements IPeopleService {
-    static $inject = ['$http', '$q'];
-    constructor(private $http: IHttpService, private $q: IQService) {
-        super();
-    }
+export default class PeopleService implements IPeopleService {
+    static $inject = ['$http', '$q', 'PwmService' ];
+    constructor(private $http: IHttpService, private $q: IQService, private pwmService: PwmService) {}
 
     autoComplete(query: string): IPromise<Person[]> {
-        return this.search(query)
-            .then((people: Person[]) => {
+        return this.search(query, { 'includeDisplayName': true })
+            .then((searchResult: SearchResult) => {
+                let people = searchResult.people;
+
                 if (people && people.length > 10) {
                     return this.$q.resolve(people.slice(0, 10));
                 }
@@ -32,15 +56,23 @@ export default class PeopleService extends PwmService implements IPeopleService
             });
     }
 
-    cardSearch(query: string): angular.IPromise<Person[]> {
-        var self = this;
+    cardSearch(query: string): angular.IPromise<SearchResult> {
+        let self = this;
+
         return this.search(query)
-            .then((people: Person[]) => {
-                var peoplePromises: IPromise<Person>[] = people.map((person: Person) => {
+            .then((searchResult: SearchResult) => {
+                let sizeExceeded = searchResult.sizeExceeded;
+
+                let peoplePromises: IPromise<Person>[] = searchResult.people.map((person: Person) => {
                     return self.getPerson(person.userKey);
                 });
 
-                return this.$q.all(peoplePromises);
+                return this.$q
+                    .all(peoplePromises)
+                    .then((people: Person[]) => {
+                        let searchResult = new SearchResult({ sizeExceeded: sizeExceeded, searchResults: people });
+                        return this.$q.resolve(searchResult);
+                    });
             });
     }
 
@@ -81,28 +113,27 @@ export default class PeopleService extends PwmService implements IPeopleService
     }
 
     getOrgChartData(personId: string): angular.IPromise<OrgChartData> {
-
-        return this.$http.get(this.getServerUrl('orgChartData'), { params: { userKey: personId } })
+        return this.$http
+            .get(this.pwmService.getServerUrl('orgChartData'), { cache: true, params: { userKey: personId } })
             .then((response) => {
                 let responseData = response.data['data'];
 
-                var manager: Person;
+                let manager: Person;
                 if ('parent' in responseData) { manager = new Person(responseData['parent']); }
-                var children = responseData['children'].map((child: any) => new Person(child));
-                var self = new Person(responseData['self']);
-
-                var orgChartData = new OrgChartData(manager, children, self);
+                const children = responseData['children'].map((child: any) => new Person(child));
+                const self = new Person(responseData['self']);
 
-                return this.$q.resolve(orgChartData);
+                return this.$q.resolve(new OrgChartData(manager, children, self));
             });
     }
 
     getPerson(id: string): IPromise<Person> {
-        return this.$http.get(this.getServerUrl('detail'), { params: { userKey: id } })
+        return this.$http
+            .get(this.pwmService.getServerUrl('detail'), { cache: true, params: { userKey: id } })
             .then((response) => {
-            let person: Person = new Person(response.data['data']);
-            return this.$q.resolve(person);
-        });
+                let person: Person = new Person(response.data['data']);
+                return this.$q.resolve(person);
+            });
     }
 
     isOrgChartEnabled(id: string): IPromise<boolean> {
@@ -110,16 +141,17 @@ export default class PeopleService extends PwmService implements IPeopleService
         return this.$q.resolve(true);
     }
 
-    search(query: string): IPromise<Person[]> {
-        return this.$http.get(this.getServerUrl('search', { 'includeDisplayName': true }), { params: { username: query } })
-        .then((response) => {
-            let people: Person[] = [];
-
-            for (let searchResult of response.data['data']['searchResults']) {
-                people.push(new Person(searchResult));
-            }
+    search(query: string, params?: any): IPromise<SearchResult> {
+        return this.$http
+            .get(
+                this.pwmService.getServerUrl('search', { 'includeDisplayName': true }),
+                { cache: true, params: { username: query } }
+            )
+            .then((response) => {
+                let receivedData: any = response.data['data'];
+                let searchResult: SearchResult = new SearchResult(receivedData);
 
-            return this.$q.resolve(people);
-        });
+                return this.$q.resolve(searchResult);
+            });
     }
 }

+ 85 - 9
src/main/angular/src/services/pwm.service.ts

@@ -1,21 +1,97 @@
-// These come from legacy PWM:
-declare var PWM_GLOBAL: any;
-declare var PWM_MAIN: any;
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2016 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+
+import { ILogService, IWindowService } from 'angular';
 
 export default class PwmService {
-    protected getServerUrl(processAction: string, additionalParameters?: any): string {
-        let url: string = PWM_GLOBAL['url-context'] + '/private/peoplesearch?processAction=' + processAction;
-        url = PwmService.addParameters(url, additionalParameters);
-        url = PWM_MAIN.addPwmFormIDtoURL(url);
+    PWM_GLOBAL: any;
+    PWM_MAIN: any;
+
+    urlContext: string;
+
+    static $inject = [ '$log', '$window' ];
+    constructor(private $log: ILogService, $window: IWindowService) {
+        this.urlContext = '';
+
+        // Search window references to PWM_GLOBAL and PWM_MAIN add by legacy PWM code
+        if ($window['PWM_GLOBAL']) {
+            this.PWM_GLOBAL = $window['PWM_GLOBAL'];
+            this.urlContext = this.PWM_GLOBAL['url-context'];
+        }
+        else {
+            this.$log.warn('PWM_GLOBAL is not defined on window');
+        }
+
+        if ($window['PWM_MAIN']) {
+            this.PWM_MAIN = $window['PWM_MAIN'];
+        }
+        else {
+            this.$log.warn('PWM_MAIN is not defined on window');
+        }
+    }
+
+    getServerUrl(processAction: string, additionalParameters?: any): string {
+        let url: string = this.urlContext + '/private/peoplesearch?processAction=' + processAction;
+        url = this.addParameters(url, additionalParameters);
+        url = this.addPwmFormIdToUrl(url);
 
         return url;
     }
 
-    private static addParameters(url: string, params: any): string {
+    get localeStrings(): any {
+        if (this.PWM_GLOBAL) {
+            return this.PWM_GLOBAL['localeStrings'];
+        }
+
+        return {};
+    }
+
+    get startupFunctions(): any[] {
+        if (this.PWM_GLOBAL) {
+            return this.PWM_GLOBAL['startupFunctions'];
+        }
+
+        return [];
+    }
+
+    private addPwmFormIdToUrl(url: string): string {
+        if (!this.PWM_MAIN) {
+            return url;
+        }
+
+        return this.PWM_MAIN.addPwmFormIDtoURL(url);
+    }
+
+
+    private addParameters(url: string, params: any): string {
+        if (!this.PWM_MAIN) {
+            return url;
+        }
+
         if (params) {
             for (var name in params) {
                 if (params.hasOwnProperty(name)) {
-                    url = PWM_MAIN.addParamToUrl(url, name, params[name]);
+                    url = this.PWM_MAIN.addParamToUrl(url, name, params[name]);
                 }
             }
         }

+ 38 - 14
src/main/angular/src/services/translations-loader.factory.ts

@@ -1,18 +1,42 @@
-import { IQService } from 'angular';
-import 'angular-translate';
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2016 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
 
-declare var PWM_GLOBAL: any;
+import 'angular-translate';
+import { IQService } from 'angular';
+import PwmService from './pwm.service';
 
-export default ['$q', ($q: IQService) => {
-    // return loaderFn
-    return function (options) {
-        var deferred = $q.defer();
+export default [
+    '$q',
+    'PwmService',
+    ($q: IQService, pwmService: PwmService) => {
+        return function () {
+            var deferred = $q.defer();
 
-        PWM_GLOBAL['startupFunctions'].push(() => {
-            deferred.resolve(PWM_GLOBAL['localeStrings']['Display']);
-        });
+            pwmService.startupFunctions.push(() => {
+                deferred.resolve(pwmService.localeStrings['Display']);
+            });
 
-        // resolve with translation data
-        return deferred.promise;
-    };
-}];
+            // resolve with translation data
+            return deferred.promise;
+        };
+    }];

+ 62 - 8
src/main/angular/src/ux/app-bar.component.scss

@@ -1,22 +1,76 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2016 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+
 $mf-app-bar-height: 26px;
 
 mf-app-bar {
   display: block;
-  height: $mf-app-bar-height;
+  min-height: $mf-app-bar-height;
+
+  // For larger displays, place the search bar inline
+  &.large {
+    > .mf-app-bar-content {
+      > mf-auto-complete,
+      > mf-search-bar {
+        margin: 2px 10px 2px 0;
+        max-width: 320px;
+        flex: 1 1 255px;
+        order: initial;
+        width: auto;
+      }
+
+      > mf-icon-button,
+      > span[flex],
+      > .page-content-title {
+        order: initial;
+      }
+    }
+  }
 
   > .mf-app-bar-content {
-    height: $mf-app-bar-height;
     display: flex;
-    flex-flow: row nowrap;
+    flex-flow: row wrap;
+
+    > mf-auto-complete,
+    > mf-search-bar {
+      order: 3;
+      width: 100%;
+    }
+
+    > mf-icon-button {
+      order: 2;
+    }
+
+    > span[flex] {
+      flex: 1 1;
+      order: 1;
+    }
 
     > .page-content-title {
       height: $mf-app-bar-height;
       line-height: $mf-app-bar-height;
       margin-right: 10px;
-    }
-
-    > [flex] {
-      flex: 1 1;
+      order: 0;
     }
   }
-}
+}

+ 36 - 0
src/main/angular/src/ux/app-bar.component.ts

@@ -1,4 +1,33 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2016 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+
 import { Component } from '../component';
+import { IAugmentedJQuery } from 'angular';
+import ElementSizeService from './element-size.service';
+
+export enum AppBarSize {
+    Large = 415
+}
 
 @Component({
     stylesheetUrl: require('ux/app-bar.component.scss'),
@@ -6,4 +35,11 @@ import { Component } from '../component';
     transclude: true
 })
 export default class AppBarComponent {
+    static $inject = [ '$element', 'MfElementSizeService' ];
+    constructor(private $element: IAugmentedJQuery, private elementSizeService: ElementSizeService) {
+    }
+
+    $onInit(): void {
+        this.elementSizeService.watchWidth(this.$element, AppBarSize);
+    }
 }

+ 0 - 5
src/main/angular/src/ux/auto-complete.component.html

@@ -1,5 +0,0 @@
-<mf-search-bar search-text="$ctrl.query"
-               ng-keydown="$ctrl.onInputKeyDown($event)"
-               ng-focus="$ctrl.onInputFocus()"
-               auto-focus></mf-search-bar>
-<ng-transclude></ng-transclude>

+ 30 - 2
src/main/angular/src/ux/auto-complete.component.scss

@@ -1,9 +1,32 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2016 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+
 mf-auto-complete {
   display: block;
+  height: 21px;
   position: relative;
 
   > mf-search-bar {
-    height: 100%;
     width: 100%;
     max-width: 100%;
   }
@@ -23,7 +46,7 @@ mf-auto-complete {
     text-align: left;
     transform: translateY(100%);
     width: 100%;
-    z-index: 1;
+    z-index: 100;
 
     > li {
       box-sizing: border-box;
@@ -40,6 +63,11 @@ mf-auto-complete {
         background-color: #eef2f2;
       }
 
+      &.search-message {
+        color: #808080;
+        text-align: center;
+      }
+
       &.selected {
         background-color: #dae1e1;
       }

+ 125 - 53
src/main/angular/src/ux/auto-complete.component.ts

@@ -1,81 +1,132 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2016 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+
 import { Component } from '../component';
-import { IAugmentedJQuery, ICompileService, IPromise, IScope } from 'angular';
+import { IAttributes, IAugmentedJQuery, IDocumentService, IPromise, IScope } from 'angular';
 
 @Component({
     bindings: {
-        'search': '&',
+        'onSearchTextChange': '&',
         'itemSelected': '&',
         'item': '@',
-        'itemText': '@'
+        'itemText': '@',
+        'searchFunction': '&search',
+        'searchText': '<'
     },
-    templateUrl: require('ux/auto-complete.component.html'),
-    transclude: true,
+    template: [
+        '$element',
+        '$attrs',
+        ($element: IAugmentedJQuery, $attrs: IAttributes): string => {
+            // Remove content template from dom
+            const contentTemplate: IAugmentedJQuery = $element.find('content-template');
+            contentTemplate.detach();
+
+            return `
+                <mf-search-bar search-text="$ctrl.searchText"
+                               on-search-text-change="$ctrl.onSearchBarTextChange(value)"                           
+                               on-key-down="$ctrl.onSearchBarKeyDown($event)"
+                               ng-click="$ctrl.onSearchBarClick($event)"
+                               auto-focus></mf-search-bar>
+                <ul class="results" ng-if="$ctrl.show" ng-click="$event.stopPropagation()">
+                    <li ng-repeat="item in $ctrl.items"
+                       ng-click="$ctrl.selectItem(item)"
+                       ng-class="{ \'selected\': $index == $ctrl.selectedIndex }\">` +
+                contentTemplate.html().replace(new RegExp($attrs['item'], 'g'), 'item') +
+                    `</li>
+                    <li class="search-message" ng-if="$ctrl.show && $ctrl.searchText && !$ctrl.items.length">
+                        <span translate="Display_SearchResultsNone"></span>
+                    </li>
+                </ul>`;
+        }],
     stylesheetUrl: require('ux/auto-complete.component.scss')
 })
 export default class AutoCompleteComponent {
     item: string;
     items: any[];
     itemSelected: (item: any) => void;
-    query: string;
-    search: (query: any) => IPromise<any[]>;
+    onSearchTextChange: Function;
+    searchText: string;
+    searchFunction: (query: any) => IPromise<any[]>;
+    searchMessage: string;
     selectedIndex: number;
     show: boolean;
 
-    static $inject = [ '$compile', '$element', '$scope' ];
-    constructor(private $compile: ICompileService,
+    static $inject = [ '$document', '$element', '$scope' ];
+    constructor(private $document: IDocumentService,
                 private $element: IAugmentedJQuery,
                 private $scope: IScope) {
     }
 
+    $onDestroy(): void {
+        this.$document.off('click', this.onDocumentClick.bind(this));
+    }
+
     $onInit(): void {
-        var self = this;
+        this.selectedIndex = -1;
 
-        this.$scope.$watch('$ctrl.query', () => {
-            self.search({ query: self.query })
-                .then((results: any[]) => {
-                    self.items = results;
-                    self.resetSelection();
-                    self.showAutoCompleteResults();
-                });
-        });
+        if (this.searchText) {
+            this.fetchAutoCompleteData(this.searchText);
+        }
 
-        this.selectedIndex = -1;
+        this.hideResults();
     }
 
     $postLink(): void {
-        // Remove content template from dom
-        var contentTemplate: IAugmentedJQuery = this.$element.find('content-template');
-        // noinspection TypeScriptUnresolvedFunction
-        contentTemplate.remove();
-
-        var autoCompleteHtml =
-            '<ul class="results" ng-if="$ctrl.show">' +
-            '   <li ng-repeat="item in $ctrl.items"' +
-            '       ng-click="$ctrl.selectItem(item)"' +
-            '       ng-class="{ \'selected\': $index == $ctrl.selectedIndex }\">' +
-            contentTemplate.html().replace(new RegExp(this.item, 'g'), 'item') +
-            '   </li>' +
-            '</ul>';
-        var compiledElement = this.$compile(autoCompleteHtml)(this.$scope);
+        var self = this;
 
-        this.$element.append(compiledElement);
+        // Listen for clicks outside of the auto-complete component
+        // Implemented as a click event instead of a blur event, so the results list can be clicked
+        this.$document.on('click', this.onDocumentClick.bind(this));
     }
 
-    onInputBlur(): void {
-        this.hideAutoCompleteResults();
+    onSearchBarClick(event: Event): void {
+        event.stopImmediatePropagation();
     }
 
-    onInputFocus(): void {
+    onSearchBarFocus(): void {
         if (this.hasItems()) {
-            this.showAutoCompleteResults();
+            this.showResults();
         }
     }
 
-    onInputKeyDown(event: KeyboardEvent): void {
+    onSearchBarTextChange(value: string): void {
+        this.searchText = value;
+        this.fetchAutoCompleteData(value);
+        this.showResults();
+
+        this.onSearchTextChange({ value: value });
+    }
+
+    onSearchBarKeyDown(event: KeyboardEvent): void {
         switch (event.keyCode) {
             case 40: // ArrowDown
-                this.selectNextItem();
-                event.preventDefault();
+                if (this.hasItems() && !this.show) {
+                    this.showResults();
+                }
+                else {
+                    this.selectNextItem();
+                    event.preventDefault();
+                }
                 break;
             case 38: // ArrowUp
                 this.selectPreviousItem();
@@ -83,43 +134,66 @@ export default class AutoCompleteComponent {
                 break;
             case 27: // Escape
                 if (!this.show || !this.hasItems()) {
-                    this.clearAutoCompleteResults();
+                    this.clearResults();
                 }
                 else {
-                    this.hideAutoCompleteResults();
+                    this.hideResults();
                 }
 
                 break;
             case 13: // Enter
                 if (this.hasItems() && this.show) {
-                    var item = this.getSelectedItem();
+                    const item = this.getSelectedItem();
                     this.selectItem(item);
                 }
                 break;
-            case 9:
+            case 9: // Tab
+                if (!this.searchText || !this.show) {
+                    return;
+                }
+
                 if (event.shiftKey) {
                     this.selectPreviousItem();
                 }
                 else {
                     this.selectNextItem();
                 }
+
                 event.preventDefault();
                 break;
         }
     }
 
     selectItem(item: any): void {
-        var data = {};
+        this.clearResults();
+
+        const data = {};
         data[this.item] = item;
         this.itemSelected(data);
     }
 
-    private clearAutoCompleteResults(): void {
+    private clearResults(): void {
         this.resetSelection();
-        this.query = null;
+        this.searchText = null;
         this.items = [];
     }
 
+    private onDocumentClick(): void {
+        if (this.show) {
+            this.hideResults();
+            this.$scope.$apply();
+        }
+    }
+
+    private fetchAutoCompleteData(value: string): void {
+        var self = this;
+        this.searchFunction({ query: value })
+            .then((results: any[]) => {
+                self.items = results;
+                self.resetSelection();
+            });
+    }
+
     private getSelectedItem(): any {
         return this.items[this.selectedIndex];
     }
@@ -128,7 +202,7 @@ export default class AutoCompleteComponent {
         return this.items && !!this.items.length;
     }
 
-    private hideAutoCompleteResults(): void  {
+    private hideResults(): void  {
         this.show = false;
     }
 
@@ -148,9 +222,7 @@ export default class AutoCompleteComponent {
         }
     }
 
-    private showAutoCompleteResults(): void  {
-        if (this.hasItems()) {
-            this.show = true;
-        }
+    private showResults(): void  {
+        this.show = true;
     }
 }

+ 47 - 0
src/main/angular/src/ux/button.component.scss

@@ -0,0 +1,47 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2016 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+
+mf-button {
+  display: inline-block;
+  margin: 10px 0;
+
+  > button {
+    background-color: #f6f9f8;
+    border: 1px solid #dae1e1;
+    border-radius: 3px;
+    color: #434c50;
+    cursor: pointer;
+    display: block;
+    font-size: 14px;
+    height: 26px;
+    line-height: 26px;
+    padding: 0 10px;
+
+    &:focus,
+    &:hover {
+      border-color: #28a9e1;
+      color: #0088ce;
+      outline: none;
+    }
+  }
+}

+ 32 - 0
src/main/angular/src/ux/button.component.ts

@@ -0,0 +1,32 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2016 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+
+import { Component } from '../component';
+
+@Component({
+    stylesheetUrl: require('ux/button.component.scss'),
+    template: '<button type="button"><ng-transclude></ng-transclude></button>',
+    transclude: true
+})
+export default class ButtonComponent {
+}

+ 6 - 0
src/main/angular/src/ux/dialog.component.html

@@ -0,0 +1,6 @@
+<div class="scrim" ng-click="$ctrl.closeDialog()"></div>
+<div class="mf-dialog-container">
+    <mf-icon-button icon="close" ng-click="$ctrl.closeDialog()"></mf-icon-button>
+
+    <ng-transclude></ng-transclude>
+</div>

+ 90 - 0
src/main/angular/src/ux/dialog.component.scss

@@ -0,0 +1,90 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2016 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+
+mf-dialog {
+  height: 100vh;
+  left: 0;
+  position: fixed;
+  top: 0;
+  width: 100vw;
+  z-index: 1000;
+
+  > .scrim {
+    background-color: rgba(0, 0, 0, .54);
+    height: 100%;
+    width: 100%;
+  }
+
+  > .mf-dialog-container {
+    background-color: white;
+    height: 100%;
+    left: 0;
+    max-height: 100vh;
+    overflow-y: auto;
+    position: absolute;
+    top: 0;
+    width: 100%;
+
+    > mf-icon-button {
+      position: absolute;
+      right: 1px;
+      top: 1px;
+    }
+
+    mf-app-bar {
+      background-color: #eaeaea;
+    }
+
+    .mf-dialog-content {
+      text-align: center;
+      padding: 10px;
+
+      > person-card {
+        max-width: 100%;
+        width: 476px;
+      }
+    }
+  }
+}
+
+@media (min-width: 420px) {
+  mf-dialog {
+    > .mf-dialog-container {
+      border: 2px solid #dddddd;
+      border-radius: 3px;
+      height: auto;
+      left: 50%;
+      max-width: 100vw;
+      top: 50%;
+      transform: translate(-50%, -50%);
+    }
+  }
+}
+
+@media (min-width: 514px) {
+  mf-dialog {
+    > .mf-dialog-container {
+      max-width: 514px;
+    }
+  }
+}

+ 61 - 0
src/main/angular/src/ux/dialog.component.ts

@@ -0,0 +1,61 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2016 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+
+import { Component } from '../component';
+import { IDocumentService } from 'angular';
+
+@Component({
+    bindings: {
+        onClose: '&'
+    },
+    stylesheetUrl: require('ux/dialog.component.scss'),
+    templateUrl: require('ux/dialog.component.html'),
+    transclude: true
+})
+export default class DialogComponent {
+    onClose: (() => void);
+    onKeyDown: ((event) => void);
+
+    static $inject = [ '$document' ];
+    constructor(private $document: IDocumentService) {
+        var self = this;
+
+        this.onKeyDown = (event) => {
+            if (event.keyCode === 27) { // ESC
+                self.closeDialog();
+            }
+        };
+    }
+
+    $onInit(): void {
+        this.$document.on('keydown', this.onKeyDown);
+    }
+
+    $onDestroy(): void {
+        this.$document.off('keydown', this.onKeyDown);
+    }
+
+    closeDialog(): void {
+        this.onClose();
+    }
+}

+ 116 - 0
src/main/angular/src/ux/dialog.service.ts

@@ -0,0 +1,116 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2016 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+
+import {
+    element,
+    IAugmentedJQuery,
+    ICompileService,
+    IControllerService,
+    IDeferred,
+    IDocumentService,
+    IPromise,
+    IQService,
+    IRootScopeService,
+    IScope,
+    ITemplateRequestService,
+    merge
+} from 'angular';
+
+export class Dialog {
+    clickOutsideToClose?: boolean;
+    controller?: string;
+    element: IAugmentedJQuery;
+    locals?: any;
+    parent?: IAugmentedJQuery;
+    parentScope?: IScope;
+    scope: IScope;
+    templateUrl: string;
+
+    constructor(options: any) {
+        this.clickOutsideToClose = options.clickOutsideToClose;
+        this.controller = options.controller;
+        this.locals = options.locals;
+        this.parent = options.parent;
+        this.parentScope = options.parentScope;
+        this.templateUrl = options.templateUrl;
+    }
+}
+
+export class DialogService {
+    private deferred: IDeferred<any>;
+    private dialog: Dialog;
+
+    static $inject = [ '$compile', '$controller', '$document', '$q', '$rootScope', '$templateRequest' ];
+    constructor(private $compile: ICompileService,
+                private $controller: IControllerService,
+                private $document: IDocumentService,
+                private $q: IQService,
+                private $rootScope: IRootScopeService,
+                private $templateRequest: ITemplateRequestService) {}
+
+    close(): void {
+        this.destroyDialog();
+        this.deferred.reject();
+    }
+
+    openDialog() {
+        var self = this;
+        var parent: IAugmentedJQuery = this.dialog.parent || this.$document.find('body');
+        this.dialog.scope = this.$rootScope.$new(false, this.dialog.parentScope);
+
+        this.$templateRequest(this.dialog.templateUrl)
+            .then((html: string) => {
+                // Create and append element to DOM
+                self.dialog.element = element(html);
+                parent.append(self.dialog.element);
+
+                self.$compile(self.dialog.element)(self.dialog.scope);
+                var controller = self.$controller(self.dialog.controller, { $scope: self.dialog.scope });
+
+                // Assign locals to constructor
+                merge(controller, self.dialog.locals);
+            });
+    }
+
+    destroyDialog() {
+        this.dialog.scope.$destroy();
+        this.dialog.scope = null;
+
+        this.dialog.element.detach();
+        this.dialog.element = null;
+
+        this.dialog = null;
+    }
+
+    show(dialog: Dialog): IPromise<any> {
+        if (this.dialog) {
+            this.close();
+        }
+        this.dialog = dialog;
+        this.deferred = this.$q.defer();
+
+        this.openDialog();
+
+        return this.deferred.promise;
+    }
+}

+ 125 - 0
src/main/angular/src/ux/element-size.service.ts

@@ -0,0 +1,125 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2016 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+
+import { element, IAugmentedJQuery, IRootScopeService, IWindowService } from 'angular';
+
+interface IResizeCallback {
+    (newValue: number, oldValue: number): void;
+}
+
+function dasherize(input: string): string {
+    return input
+        .replace(/(?:^\w|[A-Z]|\b\w)/g, function (letter, index) {
+            return (index == 0 ? '' : '-') + letter.toLowerCase();
+        })
+        .replace(/\s+/g, '');
+}
+
+class ElementSizeWatcher<T> {
+    callbacks: IResizeCallback[] = [];
+    sizes: any[] = [];
+    width: number;
+
+    constructor(public element: IAugmentedJQuery, widths: T) {
+        // Build size information
+        for (let width in widths) {
+            if (widths.hasOwnProperty(width) && !/^\d+$/.test(width)) {
+                this.sizes.push({
+                    size: widths[width],
+                    className: dasherize(width),
+                    type: width
+                });
+            }
+        }
+
+        this.sizes.sort((size1: any, size2: any) => size1.size - size2.size);
+
+        this.updateWidth();
+    }
+
+    get elementWidth(): number {
+        return this.element[0].clientWidth;
+    }
+
+    onResize(callback: IResizeCallback) {
+        this.callbacks.push(callback);
+
+        callback(this.elementWidth, this.width);
+    }
+
+    updateWidth(): void {
+        if (this.width !== this.elementWidth) {
+            this.callbacks.forEach(callback => callback(this.elementWidth, this.width));
+            this.width = this.elementWidth;
+            this.updateElementClass();
+        }
+    }
+
+    private updateElementClass(): void {
+        // Remove all size classes
+        this.sizes.forEach((size) => {
+            this.element.removeClass(size.className);
+        });
+
+        // Add applicable sizes
+        let applicableClasses = this.sizes
+            .filter((size: any) => this.width >= size.size)
+            .map((size: any) => size.className)
+            .join(' ');
+        this.element.addClass(applicableClasses);
+    }
+}
+
+export default class ElementSizeService {
+    private elementSizeWatchers: ElementSizeWatcher<any>[] = [];
+
+    static $inject = ['$rootScope', '$window'];
+    constructor(private $rootScope: IRootScopeService, private $window: IWindowService) {
+
+    }
+
+    watchWidth<T>(el: IAugmentedJQuery, widths: T): ElementSizeWatcher<T> {
+        if (!this.elementSizeWatchers.length) {
+            element(this.$window).on('resize', this.onWindowResize.bind(this));
+        }
+
+        return this.addElementSizeWatcher<T>(el, widths);
+    }
+
+    private addElementSizeWatcher<T>(element: IAugmentedJQuery, widths: T): ElementSizeWatcher<T> {
+        let elementSizeWatcher = new ElementSizeWatcher<T>(element, widths);
+        // TODO: check if element already exists
+        this.elementSizeWatchers.push(elementSizeWatcher);
+        return elementSizeWatcher;
+    }
+
+    private onWindowResize() {
+        // TODO: optimizations
+        // TODO: height (later)
+        this.elementSizeWatchers.forEach((watcher: ElementSizeWatcher<any>) => {
+            watcher.updateWidth();
+        });
+
+        this.$rootScope.$apply();
+    }
+}

+ 46 - 9
src/main/angular/src/ux/icon-button.component.scss

@@ -1,3 +1,26 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2016 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+
 $mf-icon-button-color: #808080;
 $mf-icon-button-size: 24px;
 $mf-icon-button-bg-color: transparent;
@@ -8,7 +31,6 @@ $mf-icon-button-hover-color: #0088ce;
 
 mf-icon-button {
   background-color: $mf-icon-button-bg-color;
-  border: 1px solid transparent;
   box-sizing: border-box;
   color: $mf-icon-button-color;
   cursor: pointer;
@@ -17,14 +39,29 @@ mf-icon-button {
   height: $mf-icon-button-size;
   width: $mf-icon-button-size;
 
-  &:hover {
-    background-color: $mf-icon-button-hover-bg-color;
-    border-color: $mf-icon-button-hover-border-color;
-    color: $mf-icon-button-hover-color;
-  }
-
-  > mf-icon {
+  > button {
+    background: transparent none;
+    border: 1px solid transparent;
+    border-radius: 2px;
+    color: inherit;
+    cursor: inherit;
+    display: block;
     height: 100%;
+    padding: 0;
+    margin: 0;
     width: 100%;
+
+    &:focus,
+    &:hover {
+      background-color: $mf-icon-button-hover-bg-color;
+      border-color: #dae1e1;
+      color: $mf-icon-button-hover-color;
+      outline: none;
+    }
+
+    > mf-icon {
+      height: 100%;
+      width: 100%;
+    }
   }
-}
+}

+ 24 - 1
src/main/angular/src/ux/icon-button.component.ts

@@ -1,3 +1,26 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2016 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+
 import { Component } from '../component';
 
 @Component({
@@ -5,6 +28,6 @@ import { Component } from '../component';
         icon: '@'
     },
     stylesheetUrl: require('ux/icon-button.component.scss'),
-    template: `<mf-icon icon="{{$ctrl.icon}}"></mf-icon>`
+    template: `<button type="button"><mf-icon icon="{{$ctrl.icon}}"></mf-icon></button>`
 })
 export default class IconButtonComponent {}

+ 25 - 2
src/main/angular/src/ux/icon.component.scss

@@ -1,3 +1,26 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2016 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+
 mf-icon {
   display: inline-block;
   font-size: 20px;
@@ -9,9 +32,9 @@ mf-icon {
   > i {
     color: inherit;
     position: absolute;
-    text-align: center;
+    left: 50%;
     top: 50%;
-    transform: translateY(-50%);
+    transform: translate(-50%, -50%);
     width: 100%;
 
     &:before {

+ 23 - 0
src/main/angular/src/ux/icon.component.ts

@@ -1,3 +1,26 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2016 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+
 import { Component } from '../component';
 
 @Component({

+ 6 - 3
src/main/angular/src/ux/search-bar.component.html

@@ -1,8 +1,11 @@
+<mf-icon class="search-icon" icon="magnify"></mf-icon>
 <input type="text"
+       ng-blur="$ctrl.onBlur({ $event: $event })"
+       ng-focus="$ctrl.onFocus({ $event: $event })"
+       ng-keydown="$ctrl.onKeyDown({ $event: $event })"
        ng-model="$ctrl.searchText"
-       ng-keydown="$ctrl.onInputKeyDown($event)"
+       ng-attr-placeholder="{{ ('Placeholder_Search' | translate) }}"
        title="Search Box"
        autocomplete="off" />
-<span class="placeholder" ng-hide="$ctrl.searchText">{{$ctrl.placeholder || 'Search' }}</span>
-<mf-icon icon="close" ng-click="$ctrl.clearSearchText()"></mf-icon>
+<mf-icon-button class="clear-input" icon="close" ng-click="$ctrl.clearSearchText()"></mf-icon-button>
 <!-- loader graphic -->

+ 55 - 14
src/main/angular/src/ux/search-bar.component.scss

@@ -1,3 +1,26 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2016 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+
 $mf-search-bar-height: 21px;
 
 mf-search-bar {
@@ -6,33 +29,51 @@ mf-search-bar {
   position: relative;
 
   > input {
+    border: 1px solid #dae1e1;
+    border-radius: 2px;
+    box-sizing: border-box;
+    flex: 1 1 100%;
     height: 100%;
     line-height: 100%;
-    box-sizing: border-box;
+    padding: 0 20px;
     width: 100%;
   }
 
-  > .placeholder {
-    color: rgba(0, 0, 0, 0.50);
-    left: 2px;
-    position: absolute;
-    top: 50%;
-    transform: translateY(-50%);
+  > .clear-input {
+    right: 1px;
+  }
+
+  > .search-icon {
+    margin: 0 2px;
+    left: 0;
   }
 
-  > mf-icon {
-    cursor: pointer;
+  mf-icon {
+    color: #808080;
     font-size: 16px;
+  }
+
+  > mf-icon-button {
+    > button {
+      &:focus,
+      &:hover {
+        background-color: transparent;
+        border-color: transparent;
+
+        > mf-icon {
+          color: #0088ce;
+        }
+      }
+    }
+  }
+
+  > mf-icon,
+  > mf-icon-button {
     height: $mf-search-bar-height - 2px;
     padding: 0;
     position: absolute;
-    right: 1px;
     top: 50%;
     transform: translateY(-50%);
     width: $mf-search-bar-height - 2px;
-
-    &:hover {
-      color: #0088ce;
-    }
   }
 }

+ 49 - 18
src/main/angular/src/ux/search-bar.component.ts

@@ -1,53 +1,84 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2016 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+
 import { Component } from '../component';
-import { IAugmentedJQuery, ICompileService, IScope, ITimeoutService } from 'angular';
+import { IAugmentedJQuery, ICompileService, IScope } from 'angular';
+
 
 @Component({
     bindings: {
         autoFocus: '@',
-        placeholder: '<',
-        searchText: '='
+        onSearchTextChange: '&',
+        onBlur: '&',
+        onFocus: '&',
+        onKeyDown: '&',
+        searchText: '<'
     },
     templateUrl: require('ux/search-bar.component.html'),
     stylesheetUrl: require('ux/search-bar.component.scss')
 })
 export default class SearchBarComponent {
     autoFocus: boolean;
+    focused: boolean;
+    onSearchTextChange: Function;
     searchText: string;
 
-    static $inject = [ '$compile', '$element', '$scope', '$timeout' ];
+    static $inject = [ '$compile', '$element', '$scope' ];
     constructor(private $compile: ICompileService,
                 private $element: IAugmentedJQuery,
-                private $scope: IScope,
-                private $timeout: ITimeoutService) {
+                private $scope: IScope) {
     }
 
     $onInit(): void {
+        this.autoFocus = this.autoFocus !== undefined;
+
         var self = this;
 
-        this.autoFocus = this.autoFocus !== undefined;
+        this.$scope.$watch('$ctrl.searchText', (newValue: string, oldValue: string) => {
+            if (newValue === oldValue) {
+                return;
+            }
 
+            self.onSearchTextChange({ value: newValue });
+        });
+    }
+
+    $postLink() {
+        const self = this;
         if (this.autoFocus) {
-            this.$timeout(() => {
+            this.$scope.$evalAsync(() => {
                 self.focusInput();
-            }, 100);
+            });
         }
     }
 
     clearSearchText(): void {
         this.searchText = '';
+        this.$element.find('input').val('');
         this.focusInput();
     }
 
     focusInput() {
         this.$element.find('input')[0].focus();
     }
-
-    onInputKeyDown(event: KeyboardEvent): void {
-        switch (event.keyCode) {
-            case 27: // Escape
-                this.clearSearchText();
-
-                break;
-        }
-    }
 }

+ 23 - 0
src/main/angular/src/ux/table-column.directive.ts

@@ -1,3 +1,26 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2016 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+
 import { IAttributes, IAugmentedJQuery, IDirective, IScope } from 'angular';
 import TableDirectiveController from './table.directive.controller';
 

+ 70 - 25
src/main/angular/src/ux/table.directive.controller.ts

@@ -1,60 +1,106 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2016 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+
 import { IScope } from 'angular';
 import Column from './../models/column.model';
+import { IFilterService } from 'angular';
+import * as angular from 'angular';
 
 export default class TableDirectiveController {
     columns: Column[];
     items: any[];
     itemName: string;
     onClickItem: (scope: IScope, locals: any) => void;
+    searchHighlight: string;
     showConfiguration: boolean = false;
     sortColumn: Column;
-    sortReverse: boolean = false;
+    reverseSort: boolean = false;
 
-    static $inject = [ '$scope' ];
-    constructor(private $scope: IScope) {
+    static $inject = [ '$filter', '$scope' ];
+    constructor(private $filter: IFilterService, private $scope: IScope) {
         this.columns = [];
     }
 
-    $onInit(): void {
-
-    }
-
     addColumn(label: string, valueExpression: string): void {
         this.columns.push(new Column(label, valueExpression));
     }
 
-    clickItem(item: any) {
-        var locals = {};
+    clickItem(item: any, event: Event) {
+        const locals = {};
         locals[this.itemName] = item;
 
         this.onClickItem(this.$scope, locals);
+
+        event.stopImmediatePropagation();
     }
 
     getItems(): any[] {
-        if (this.sortColumn) {
-            var self = this;
+        if (this.items && this.sortColumn) {
+            const self = this;
+
+            return this.items.concat().sort((item1: any, item2: any): number => {
+                const value1 = self.getValue(item1, self.sortColumn.valueExpression);
+                const value2 = self.getValue(item2, self.sortColumn.valueExpression);
 
-            return this.items.sort((item1: any, item2: any): number => {
-                var value1 = this.getValue(item1, self.sortColumn.valueExpression);
-                var value2 = this.getValue(item2, self.sortColumn.valueExpression);
+                let result = 0;
 
-                var result = 0;
-                if (value1 < value2) {
+                // value 1 is undefined but not value 2
+                if (angular.isUndefined(value1) && !angular.isUndefined(value2)) {
                     result = -1;
                 }
-                else if (value1 > value2) {
+                // value 2 is undefined but not value 1
+                else if (angular.isUndefined(value2) && !angular.isUndefined(value1)) {
                     result = 1;
                 }
+                // Both values are numbers
+                else if (angular.isNumber(value1) && angular.isNumber(value2)) {
+                    result = value1 - value2;
+                }
+                // Both values are strings
+                else if (angular.isString(value1) && angular.isString(value2)) {
+                    result = value1.localeCompare(value2);
+                }
 
-                return self.sortReverse ? -result : result;
+                return self.reverseSort ? -result : result;
             });
         }
 
         return this.items;
     }
 
-    getValue(item: any, valueExpression: string) {
-        var locals: any = {};
+    // getDisplayValue(item: any, valueExpression: string): any {
+    //     let value = this.getValue(item, valueExpression);
+    //
+    //     if (this.searchHighlight) {
+    //         return this
+    //             .$filter<(input: string, searchText: string) => string>('highlight')(value, this.searchHighlight);
+    //     }
+    //
+    //     return value;
+    // }
+
+    getValue(item: any, valueExpression: string): any {
+        const locals: any = {};
         // itemName comes from directive's link function
         locals[this.itemName] = item;
 
@@ -71,12 +117,11 @@ export default class TableDirectiveController {
 
     sortOnColumn(column: Column): void {
         if (this.sortColumn === column) {
-            // Reverse sort order if the column has already been sorted
-            this.sortReverse = !this.sortReverse;
+            this.toggleSortOrder();
         }
         else {
             // Reset sort order to normal sort order
-            this.sortReverse = false;
+            this.reverseSort = false;
         }
 
         this.sortColumn = column;
@@ -88,7 +133,7 @@ export default class TableDirectiveController {
         event.stopImmediatePropagation();
     }
 
-    reverseSort(): void {
-        this.sortOnColumn(this.sortColumn);
+    toggleSortOrder(): void {
+        this.reverseSort = !this.reverseSort;
     }
 }

+ 7 - 7
src/main/angular/src/ux/table.directive.html

@@ -19,19 +19,19 @@
                 <div class="column-header">
                     <span class="label" ng-bind="column.label" ng-click="table.sortOnColumn(column)"></span>
                     <mf-icon icon="chevron-down"
-                             ng-if="table.sortColumn === column && table.sortReverse"
-                             ng-click="table.reverseSort()"></mf-icon>
+                             ng-if="table.sortColumn === column && table.reverseSort"
+                             ng-click="table.toggleSortOrder()"></mf-icon>
                     <mf-icon icon="chevron-up"
-                             ng-if="table.sortColumn === column && !table.sortReverse"
-                             ng-click="table.reverseSort()"></mf-icon>
+                             ng-if="table.sortColumn === column && !table.reverseSort"
+                             ng-click="table.toggleSortOrder()"></mf-icon>
                 </div>
             </th>
         </tr>
     </thead>
     <tbody>
-        <tr ng-repeat="item in table.getItems()"
-            ng-click="table.clickItem(item)">
-            <td ng-repeat="column in table.getVisibleColumns()" ng-bind="table.getValue(item, column.valueExpression)"></td>
+        <tr ng-repeat="item in table.getItems()" ng-click="table.clickItem(item, $event)">
+            <td ng-repeat="column in table.getVisibleColumns()"
+                ng-bind="table.getValue(item, column.valueExpression)"></td>
         </tr>
     </tbody>
 </table>

+ 25 - 0
src/main/angular/src/ux/table.directive.scss

@@ -1,3 +1,26 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2016 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+
 mf-table {
   display: block;
   width: 100%;
@@ -8,6 +31,7 @@ mf-table {
 
   > .table-configuration {
     position: relative;
+    z-index: 100;
 
     > ul {
       background-color: #ffffff;
@@ -18,6 +42,7 @@ mf-table {
       margin: 0;
       padding: 10px;
       position: absolute;
+      text-align: left;
       top: 0;
 
       > li {

+ 40 - 7
src/main/angular/src/ux/table.directive.ts

@@ -1,8 +1,31 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2016 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+
 import { IAttributes, IAugmentedJQuery, IDirective, IDocumentService, IParseService, IScope } from 'angular';
 import TableDirectiveController from './table.directive.controller';
 
 require('ux/table.directive.scss');
-var templateUrl = require('ux/table.directive.html');
+const templateUrl = require('ux/table.directive.html');
 
 class DataExpression {
     constructor(public itemName: string,
@@ -26,20 +49,30 @@ class TableDirective implements IDirective {
             controller.onClickItem = this.$parse(instanceAttributes['onClickItem']);
         }
 
-        var dataExpression: DataExpression = this.parseDataExpression(instanceAttributes['data']);
+        const dataExpression: DataExpression = this.parseDataExpression(instanceAttributes['data']);
 
         controller.itemName = dataExpression.itemName;
         // Collection may not be immediately available (i.e. promise). Watch its value for changes
         $scope.$watch(dataExpression.collectionExpression, (items: any[]) => {
             controller.items = items;
         });
+        $scope.$watch(instanceAttributes['searchHighlight'], (searchHighlight: string) => {
+            controller.searchHighlight = searchHighlight;
+        });
 
         // Listen for clicks outside of the configuration panel
-        this.$document.bind('click', (event: Event) => {
-            controller.hideConfiguration();
-            $scope.$apply();
+        this.$document.on('click', () => {
+            if (controller.showConfiguration) {
+                controller.hideConfiguration();
+                $scope.$apply();
+            }
+        });
 
-            event.stopImmediatePropagation();
+        this.$document.on('keydown', (event) => {
+            if (event.keyCode === 27) { // Escape
+                controller.hideConfiguration();
+                $scope.$apply();
+            }
         });
 
         // Clean up event listeners
@@ -50,7 +83,7 @@ class TableDirective implements IDirective {
 
     parseDataExpression(dataExpression: string): any {
         // Parse data expression from [data] attribute
-        var match: RegExpMatchArray = dataExpression.match(/^\s*(.+)\s+in\s+(.*?)\s*$/);
+        let match: RegExpMatchArray = dataExpression.match(/^\s*(.+)\s+in\s+(.*?)\s*$/);
         if (!match) {
             throw Error('Expected expression in [data] attribute in form of "[ITEM] in [COLLECTION]"');
         }

+ 32 - 1
src/main/angular/src/ux/ux.module.ts

@@ -1,21 +1,52 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2016 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+
 import { module } from 'angular';
 import AppBarComponent from './app-bar.component';
 import AutoCompleteComponent from './auto-complete.component';
+import ButtonComponent from './button.component';
+import DialogComponent from './dialog.component';
+// import { DialogService } from './dialog.service';
 import IconButtonComponent from './icon-button.component';
 import IconComponent from './icon.component';
 import SearchBarComponent from './search-bar.component';
 import TableDirectiveFactory from './table.directive';
 import TableColumnDirectiveFactory from './table-column.directive';
+import ElementSizeService from './element-size.service';
 
 var moduleName = 'peoplesearch.ux';
 
 module(moduleName, [ ])
     .component('mfAppBar', AppBarComponent)
     .component('mfAutoComplete', AutoCompleteComponent)
+    .component('mfButton', ButtonComponent)
+    .component('mfDialog', DialogComponent)
     .component('mfIconButton', IconButtonComponent)
     .component('mfIcon', IconComponent)
     .component('mfSearchBar', SearchBarComponent)
     .directive('mfTable', TableDirectiveFactory)
-    .directive('mfTableColumn', TableColumnDirectiveFactory);
+    .directive('mfTableColumn', TableColumnDirectiveFactory)
+    .service('MfElementSizeService', ElementSizeService);
+    // .service('MfDialogService', DialogService);
 
 export default moduleName;

+ 5 - 0
src/main/angular/tslint.json

@@ -7,6 +7,10 @@
     ],
     "curly": true,
     "eofline": true,
+    "file-header": [
+      true,
+      "Copyright \\(c\\)"
+    ],
     "indent": [
       true,
       "spaces"
@@ -16,6 +20,7 @@
       120
     ],
     "no-bitwise": true,
+    "no-console": [true, "log", "error"],
     "no-duplicate-variable": true,
     "no-eval": true,
     "no-arg": true,

+ 33 - 1
src/main/angular/webpack.build.js

@@ -1,7 +1,32 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2016 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+
 var commonConfig = require('./webpack.common.js');
+var webpack = require('webpack');
 var webpackMerge = require('webpack-merge');
 
 module.exports = webpackMerge(commonConfig, {
+    devtool: 'source-map',
     entry: {
         'peoplesearch.ng': './src/main'
     },
@@ -18,5 +43,12 @@ module.exports = webpackMerge(commonConfig, {
                 ]
             }
         ]
-    }
+    },
+    plugins: [
+        new webpack.optimize.UglifyJsPlugin({
+            compress: { warnings: false },
+            comments: false,
+            sourceMap: true
+        })
+    ]
 });

+ 24 - 7
src/main/angular/webpack.common.js

@@ -1,5 +1,27 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2016 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+
 var HtmlWebpackPlugin = require('html-webpack-plugin');
-var WriteFileWebpackPlugin = require('write-file-webpack-plugin');
 var autoPrefixer = require('autoprefixer');
 var path = require('path');
 var webpack = require('webpack');
@@ -13,7 +35,6 @@ module.exports = {
         port: 4000,
         historyApiFallback: true
     },
-    devtool: 'cheap-module-source-map',
     // Externals copied to /dist via CopyWebpackPlugin
     externals:
     {
@@ -68,11 +89,7 @@ module.exports = {
         new HtmlWebpackPlugin({
             template: 'index.html',
             inject: 'body'
-        }),
-
-        // Because we copy the output to another directory, we need file system watch support.
-        // Webpack-dev-server does not do this without the plugin.
-        new WriteFileWebpackPlugin()
+        })
     ],
     postcss: function() {
         return [

+ 24 - 0
src/main/angular/webpack.dev.js

@@ -1,8 +1,32 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2016 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+
 var commonConfig = require('./webpack.common.js');
 var CopyWebpackPlugin = require('copy-webpack-plugin');
 var webpackMerge = require('webpack-merge');
 
 module.exports = webpackMerge(commonConfig, {
+    devtool: 'cheap-module-source-map',
     entry: {
         'peoplesearch.ng': './src/main.dev'
     },

+ 23 - 0
src/main/angular/webpack.test.js

@@ -1,3 +1,26 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2016 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+
 var commonConfig = require('./webpack.common.js');
 var webpackMerge = require('webpack-merge');
 

+ 1 - 1
src/main/resources/password/pwm/AppProperty.properties

@@ -79,7 +79,7 @@ http.resources.expirationSeconds=3600
 http.resources.gzip.enable=true
 http.resources.pathNonceEnable=true
 http.resources.pathNoncePrefix=nonce-
-http.resources.webjarMappings={"/public/resources/dojo/dgrid/":"/webjars/dgrid/1.1.0/","/public/resources/dojo/dojo/":"/webjars/dojo/1.11.2/","/public/resources/dojo/dijit/":"/webjars/dijit/1.11.2/","/public/resources/dojo/dojox/":"/webjars/dojox/1.11.2/","/public/resources/font/font-awesome/":"/webjars/font-awesome/4.7.0/","/public/resources/flags/":"/webjars/famfamfam-flags/1.0.0/dist/","/public/resources/angular/":"/webjars/angular/1.5.8/","/public/resources/angular-ui-router/":"/webjars/angular-ui-router/1.0.0-beta.3/","/public/resources/angular-translate/":"/webjars/angular-translate/2.13.0/"}
+http.resources.webjarMappings={"/public/resources/dojo/dgrid/":"/webjars/dgrid/1.1.0/","/public/resources/dojo/dojo/":"/webjars/dojo/1.11.2/","/public/resources/dojo/dijit/":"/webjars/dijit/1.11.2/","/public/resources/dojo/dojox/":"/webjars/dojox/1.11.2/","/public/resources/font/font-awesome/":"/webjars/font-awesome/4.7.0/","/public/resources/flags/":"/webjars/famfamfam-flags/1.0.0/dist/","/public/resources/angular/":"/webjars/angular/1.5.8/","/public/resources/angular-ui-router/":"/webjars/angular-ui-router/1.0.0-beta.3/","/public/resources/angular-translate/":"/webjars/angular-translate/2.13.0/","/public/resources/angular-sanitize/":"/webjars/angular-sanitize/1.5.8/"}
 http.resources.zipFiles=[]
 http.gzip.enable=true
 http.errors.allowHtml=true

+ 1 - 0
src/main/webapp/WEB-INF/jsp/peoplesearch.jsp

@@ -42,6 +42,7 @@
 
 <%-- TODO: change these to the 'min' versions (i.e. angular.min.js) --%>
 <pwm:script-ref url="/public/resources/angular/angular.js" />
+<pwm:script-ref url="/public/resources/angular-sanitize/angular-sanitize.js" />
 <pwm:script-ref url="/public/resources/angular-ui-router/release/angular-ui-router.js" />
 <pwm:script-ref url="/public/resources/angular-translate/dist/angular-translate.js" />
 

+ 1 - 1
src/main/webapp/private/index.jsp

@@ -70,7 +70,7 @@
 
             <pwm:if test="<%=PwmIfTest.orgChartEnabled%>">
                 <pwm:if test="<%=PwmIfTest.permission%>" permission="<%=Permission.PEOPLE_SEARCH%>">
-                    <a id="button_PeopleSearch" href="<pwm:url addContext="true" url='<%=PwmServletDefinition.PeopleSearch.servletUrl()%>'/>/orgchart/">
+                    <a id="button_PeopleSearch" href="<pwm:url addContext="true" url='<%=PwmServletDefinition.PeopleSearch.servletUrl()%>'/>/orgchart">
                         <div class="tile">
                             <div class="tile-content">
                                 <div class="tile-image orgchart-image"></div>

Some files were not shown because too many files changed in this diff