Преглед изворни кода

Merge branch 'master' into l10n-ldap

jrivard@gmail.com пре 6 година
родитељ
комит
20810f4686
55 измењених фајлова са 1912 додато и 1786 уклоњено
  1. 129 615
      client/package-lock.json
  2. 1 1
      client/package.json
  3. 31 29
      client/src/components/changepassword/autogen-change-password.component.html
  4. 20 18
      client/src/components/changepassword/random-change-password.component.html
  5. 26 24
      client/src/components/changepassword/success-change-password.component.html
  6. 66 64
      client/src/components/changepassword/type-change-password.component.html
  7. 15 1
      client/src/i18n/translations_en.json
  8. 40 37
      client/src/modules/helpdesk/helpdesk-detail-dialog.template.html
  9. 160 158
      client/src/modules/helpdesk/helpdesk-detail.component.scss
  10. 66 65
      client/src/modules/helpdesk/helpdesk-search.component.scss
  11. 4 2
      client/src/modules/helpdesk/helpdesk.module.ts
  12. 42 40
      client/src/modules/helpdesk/recent-verifications-dialog.template.html
  13. 83 81
      client/src/modules/helpdesk/verifications-dialog.template.html
  14. 54 0
      client/src/modules/peoplesearch/orgchart-email.component.html
  15. 12 0
      client/src/modules/peoplesearch/orgchart-email.component.scss
  16. 73 0
      client/src/modules/peoplesearch/orgchart-email.controller.ts
  17. 50 0
      client/src/modules/peoplesearch/orgchart-export.component.html
  18. 2 0
      client/src/modules/peoplesearch/orgchart-export.component.scss
  19. 53 0
      client/src/modules/peoplesearch/orgchart-export.controller.ts
  20. 6 1
      client/src/modules/peoplesearch/orgchart-search.component.html
  21. 12 11
      client/src/modules/peoplesearch/orgchart-search.component.scss
  22. 13 1
      client/src/modules/peoplesearch/orgchart-search.component.ts
  23. 285 284
      client/src/modules/peoplesearch/orgchart.component.scss
  24. 48 46
      client/src/modules/peoplesearch/peoplesearch-cards.component.scss
  25. 18 17
      client/src/modules/peoplesearch/peoplesearch-table.component.scss
  26. 8 2
      client/src/modules/peoplesearch/peoplesearch.module.ts
  27. 90 106
      client/src/modules/peoplesearch/peoplesearch.scss
  28. 79 67
      client/src/modules/peoplesearch/person-details-dialog.component.html
  29. 77 67
      client/src/modules/peoplesearch/person-details-dialog.component.scss
  30. 59 5
      client/src/modules/peoplesearch/person-details-dialog.component.ts
  31. 8 1
      client/src/services/base-config.service.ts
  32. 21 1
      client/src/services/people.service.ts
  33. 36 2
      client/src/services/peoplesearch-config.service.ts
  34. 14 0
      client/src/styles.scss
  35. 0 5
      data-service/pom.xml
  36. 1 1
      docker/pom.xml
  37. 1 1
      onejar/pom.xml
  38. 4 4
      pom.xml
  39. 0 6
      rest-test-service/pom.xml
  40. 3 3
      server/pom.xml
  41. 1 0
      server/src/main/java/password/pwm/AppProperty.java
  42. 6 0
      server/src/main/java/password/pwm/PwmApplication.java
  43. 4 0
      server/src/main/java/password/pwm/config/PwmSetting.java
  44. 9 0
      server/src/main/java/password/pwm/http/servlet/peoplesearch/PeopleSearchClientConfigBean.java
  45. 15 0
      server/src/main/java/password/pwm/http/servlet/peoplesearch/PeopleSearchConfiguration.java
  46. 29 15
      server/src/main/java/password/pwm/http/servlet/peoplesearch/PeopleSearchDataReader.java
  47. 89 0
      server/src/main/java/password/pwm/http/servlet/peoplesearch/PeopleSearchService.java
  48. 30 1
      server/src/main/java/password/pwm/http/servlet/peoplesearch/PeopleSearchServlet.java
  49. 1 0
      server/src/main/java/password/pwm/svc/AbstractPwmService.java
  50. 1 0
      server/src/main/java/password/pwm/svc/PwmServiceEnum.java
  51. 1 0
      server/src/main/resources/password/pwm/AppProperty.properties
  52. 10 0
      server/src/main/resources/password/pwm/config/PwmSetting.xml
  53. 4 0
      server/src/main/resources/password/pwm/i18n/PwmSetting.properties
  54. 1 2
      webapp/src/main/webapp/WEB-INF/jsp/helpdesk.jsp
  55. 1 2
      webapp/src/main/webapp/WEB-INF/jsp/peoplesearch.jsp

Разлика између датотеке није приказан због своје велике величине
+ 129 - 615
client/package-lock.json


+ 1 - 1
client/package.json

@@ -18,7 +18,7 @@
     "author": "",
     "license": "ISC",
     "dependencies": {
-        "@microfocus/ias-icons": "1.0.0",
+        "@microfocus/ias-icons": "1.0.1",
         "@microfocus/ng-ias": "1.0.0-alpha.2",
         "@microfocus/ux-ias": "1.0.0-rc",
         "@uirouter/angularjs": "1.0.15",

+ 31 - 29
client/src/components/changepassword/autogen-change-password.component.html

@@ -20,36 +20,38 @@
   ~ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
   -->
 
-<div class="ias-dialog autogen-change-password-dialog">
-    <div class="ias-dialog-container">
-        <div class="ias-dialog-label">
-            <div class="ias-title" ng-bind="'Title_RandomPasswords' | translate"></div>
-        </div>
-        <div class="ias-dialog-content">
-            <p ng-bind="'Display_PasswordGeneration' | translate"></p>
-            <table>
-                <tbody>
-                <tr ng-repeat="i in [0,2,4,6,8,10,12,14,16,18]">
-                    <td ng-repeat="j in [i, i+1]">
-                        <div ng-bind="$ctrl.passwordSuggestions[j]" ng-click="$ctrl.onChoosePasswordSuggestion(j)">
-                        </div>
-                    </td>
-                </tr>
-                </tbody>
-            </table>
-        </div>
-        <div class="ias-actions">
-            <ias-button ng-click="$ctrl.populatePasswordSuggestions()"
-                        ng-disabled="$ctrl.fetchingRandoms">{{ 'Button_More' | translate }}
+<div class="ias-styles-root">
+    <div class="ias-dialog autogen-change-password-dialog">
+        <div class="ias-dialog-container">
+            <div class="ias-dialog-label">
+                <div class="ias-title" ng-bind="'Title_RandomPasswords' | translate"></div>
+            </div>
+            <div class="ias-dialog-content">
+                <p ng-bind="'Display_PasswordGeneration' | translate"></p>
+                <table>
+                    <tbody>
+                    <tr ng-repeat="i in [0,2,4,6,8,10,12,14,16,18]">
+                        <td ng-repeat="j in [i, i+1]">
+                            <div ng-bind="$ctrl.passwordSuggestions[j]" ng-click="$ctrl.onChoosePasswordSuggestion(j)">
+                            </div>
+                        </td>
+                    </tr>
+                    </tbody>
+                </table>
+            </div>
+            <div class="ias-actions">
+                <ias-button ng-click="$ctrl.populatePasswordSuggestions()"
+                            ng-disabled="$ctrl.fetchingRandoms">{{ 'Button_More' | translate }}
+                </ias-button>
+                <ias-button ng-click="cancel()">{{ 'Button_Cancel' | translate }}</ias-button>
+            </div>
+
+            <ias-button class="ias-icon-button ias-dialog-cancel-button"
+                        id="close-icon"
+                        ng-attr-title="{{ 'Button_CloseWindow' | translate }}"
+                        ng-click="cancel()">
+                <ias-icon icon="close_thick"></ias-icon>
             </ias-button>
-            <ias-button ng-click="cancel()">{{ 'Button_Cancel' | translate }}</ias-button>
         </div>
-
-        <ias-button class="ias-icon-button ias-dialog-cancel-button"
-                    id="close-icon"
-                    ng-attr-title="{{ 'Button_CloseWindow' | translate }}"
-                    ng-click="cancel()">
-            <ias-icon icon="close_thick"></ias-icon>
-        </ias-button>
     </div>
 </div>

+ 20 - 18
client/src/components/changepassword/random-change-password.component.html

@@ -20,26 +20,28 @@
   ~ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
   -->
 
-<div class="ias-dialog random-change-password-dialog">
-    <div class="ias-dialog-container">
-        <div class="ias-dialog-label">
-            <div class="ias-title" ng-bind="'Title_ChangePassword' | translate"></div>
-        </div>
+<div class="ias-styles-root">
+    <div class="ias-dialog random-change-password-dialog">
+        <div class="ias-dialog-container">
+            <div class="ias-dialog-label">
+                <div class="ias-title" ng-bind="'Title_ChangePassword' | translate"></div>
+            </div>
 
-        <div class="ias-dialog-content">
-            <p ng-bind="'Display_SetRandomPasswordPrompt' | translate"></p>
-        </div>
+            <div class="ias-dialog-content">
+                <p ng-bind="'Display_SetRandomPasswordPrompt' | translate"></p>
+            </div>
 
-        <div class="ias-actions">
-            <ias-button ng-click="$ctrl.confirmSetRandomPassword()">{{ 'Button_OK' | translate }}</ias-button>
-            <ias-button ng-click="cancel()">{{ 'Button_Cancel' | translate }}</ias-button>
-        </div>
+            <div class="ias-actions">
+                <ias-button ng-click="$ctrl.confirmSetRandomPassword()">{{ 'Button_OK' | translate }}</ias-button>
+                <ias-button ng-click="cancel()">{{ 'Button_Cancel' | translate }}</ias-button>
+            </div>
 
-        <ias-button class="ias-icon-button ias-dialog-cancel-button"
-                    id="close-icon"
-                    ng-attr-title="{{ 'Button_CloseWindow' | translate }}"
-                    ng-click="cancel()">
-            <ias-icon icon="close_thick"></ias-icon>
-        </ias-button>
+            <ias-button class="ias-icon-button ias-dialog-cancel-button"
+                        id="close-icon"
+                        ng-attr-title="{{ 'Button_CloseWindow' | translate }}"
+                        ng-click="cancel()">
+                <ias-icon icon="close_thick"></ias-icon>
+            </ias-button>
+        </div>
     </div>
 </div>

+ 26 - 24
client/src/components/changepassword/success-change-password.component.html

@@ -20,35 +20,37 @@
   ~ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
   -->
 
-<div class="ias-dialog success-change-password-dialog">
-    <div class="ias-dialog-container">
-        <div class="ias-dialog-label">
-            <div class="ias-title" ng-bind="'Title_ChangePassword' | translate"></div>
-        </div>
+<div class="ias-styles-root">
+    <div class="ias-dialog success-change-password-dialog">
+        <div class="ias-dialog-container">
+            <div class="ias-dialog-label">
+                <div class="ias-title" ng-bind="'Title_ChangePassword' | translate"></div>
+            </div>
+
+            <div class="ias-dialog-content">
+                <p ng-bind="$ctrl.successMessage"></p>
+                <div ng-if="$ctrl.displayNewPassword">
+                    <span ng-bind="'Field_NewPassword' | translate"></span>
+                    <ias-button ng-click="$ctrl.togglePasswordMasked()" ng-if="$ctrl.maskPasswords">
+                        {{ 'Button_Show' | translate }}
+                    </ias-button>
+                    <input ng-model="$ctrl.password" ng-hide="$ctrl.passwordMasked" readonly type="text" autofocus>
+                </div>
+            </div>
 
-        <div class="ias-dialog-content">
-            <p ng-bind="$ctrl.successMessage"></p>
-            <div ng-if="$ctrl.displayNewPassword">
-                <span ng-bind="'Field_NewPassword' | translate"></span>
-                <ias-button ng-click="$ctrl.togglePasswordMasked()" ng-if="$ctrl.maskPasswords">
-                    {{ 'Button_Show' | translate }}
+            <div class="ias-actions">
+                <ias-button ng-click="cancel()">{{ 'Button_OK' | translate }}</ias-button>
+                <ias-button ng-click="$ctrl.clearAnswers()"
+                           ng-if="$ctrl.clearResponsesSetting==='ask'">{{ 'Button_ClearResponses' | translate }}
                 </ias-button>
-                <input ng-model="$ctrl.password" ng-hide="$ctrl.passwordMasked" readonly type="text" autofocus>
             </div>
-        </div>
 
-        <div class="ias-actions">
-            <ias-button ng-click="cancel()">{{ 'Button_OK' | translate }}</ias-button>
-            <ias-button ng-click="$ctrl.clearAnswers()"
-                       ng-if="$ctrl.clearResponsesSetting==='ask'">{{ 'Button_ClearResponses' | translate }}
+            <ias-button class="ias-icon-button ias-dialog-cancel-button"
+                        id="close-icon"
+                        ng-attr-title="{{ 'Button_CloseWindow' | translate }}"
+                        ng-click="cancel()">
+                <ias-icon icon="close_thick"></ias-icon>
             </ias-button>
         </div>
-
-        <ias-button class="ias-icon-button ias-dialog-cancel-button"
-                    id="close-icon"
-                    ng-attr-title="{{ 'Button_CloseWindow' | translate }}"
-                    ng-click="cancel()">
-            <ias-icon icon="close_thick"></ias-icon>
-        </ias-button>
     </div>
 </div>

+ 66 - 64
client/src/components/changepassword/type-change-password.component.html

@@ -20,72 +20,74 @@
   ~ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
   -->
 
-<div class="ias-dialog type-change-password-dialog">
-    <div class="ias-dialog-container">
-        <div class="ias-dialog-label">
-            <div class="ias-title" ng-bind="'Title_ChangePassword' | translate"></div>
-        </div>
-        <div class="ias-dialog-content">
-            <p ng-bind="$ctrl.message"></p>
+<div class="ias-styles-root">
+    <div class="ias-dialog type-change-password-dialog">
+        <div class="ias-dialog-container">
+            <div class="ias-dialog-label">
+                <div class="ias-title" ng-bind="'Title_ChangePassword' | translate"></div>
+            </div>
+            <div class="ias-dialog-content">
+                <p ng-bind="$ctrl.message"></p>
 
-            <table ng-model-options="{ debounce: $ctrl.inputDebounce }">
-                <tbody>
-                <tr>
-                    <td>
-                        <input autofocus ng-model="$ctrl.password1" ng-hide="$ctrl.passwordMasked" type="text">
-                        <input autofocus ng-model="$ctrl.password1" ng-show="$ctrl.passwordMasked" type="password">
-                    </td>
-                    <td>
-                        <div class="strength-meter"
-                             ng-attr-title="{{ 'Display_StrengthMeter' | translate }}"
-                             ng-if="$ctrl.showStrengthMeter">
-                            <ias-icon class="strength-base" icon="strength5"></ias-icon>
-                            <ias-icon class="strength" icon="strength1" ng-show="$ctrl.strength===1"></ias-icon>
-                            <ias-icon class="strength" icon="strength2" ng-show="$ctrl.strength===2"></ias-icon>
-                            <ias-icon class="strength" icon="strength3" ng-show="$ctrl.strength===3"></ias-icon>
-                            <ias-icon class="strength" icon="strength4" ng-show="$ctrl.strength===4"></ias-icon>
-                            <ias-icon class="strength" icon="strength5" ng-show="$ctrl.strength===5"></ias-icon>
-                        </div>
-                    </td>
-                </tr>
-                <tr>
-                    <td>
-                        <input ng-model="$ctrl.password2" ng-hide="$ctrl.passwordMasked" type="text">
-                        <input ng-model="$ctrl.password2" ng-show="$ctrl.passwordMasked" type="password">
-                        <div id="password-icon"
-                             ng-attr-title="{{ 'Button_Show' | translate }}"
-                             ng-if="$ctrl.maskPasswords">
-                                <a ng-bind="'Button_Show' | translate"
-                                   ng-click="$ctrl.togglePasswordMasked()"
-                                   tabindex="0"></a>
-                        </div>
-                    </td>
-                    <td>
-                        <ias-icon icon="status_ok_thin"
-                                  ng-attr-title="{{ 'Display_MatchCondition' | translate }}"
-                                  ng-show="$ctrl.matchStatus==='MATCH'"></ias-icon>
-                        <ias-icon icon="message_error_thick"
-                                  ng-attr-title="{{ 'Display_MatchCondition' | translate }}"
-                                  ng-show="$ctrl.matchStatus==='NO_MATCH'"></ias-icon>
-                    </td>
-                </tr>
-                </tbody>
-            </table>
-        </div>
-        <div class="ias-actions">
-            <ias-button ng-click="$ctrl.chooseTypedPassword()"
-                        ng-disabled="!$ctrl.passwordAcceptable">{{ 'Button_ChangePassword' | translate }}
-            </ias-button>
-            <ias-button ng-click="$ctrl.onClickRandomPasswords()"
-                        ng-if="$ctrl.passwordUiMode === 'both'">{{ 'Title_RandomPasswords' | translate }}
+                <table ng-model-options="{ debounce: $ctrl.inputDebounce }">
+                    <tbody>
+                    <tr>
+                        <td>
+                            <input autofocus ng-model="$ctrl.password1" ng-hide="$ctrl.passwordMasked" type="text">
+                            <input autofocus ng-model="$ctrl.password1" ng-show="$ctrl.passwordMasked" type="password">
+                        </td>
+                        <td>
+                            <div class="strength-meter"
+                                 ng-attr-title="{{ 'Display_StrengthMeter' | translate }}"
+                                 ng-if="$ctrl.showStrengthMeter">
+                                <ias-icon class="strength-base" icon="strength5"></ias-icon>
+                                <ias-icon class="strength" icon="strength1" ng-show="$ctrl.strength===1"></ias-icon>
+                                <ias-icon class="strength" icon="strength2" ng-show="$ctrl.strength===2"></ias-icon>
+                                <ias-icon class="strength" icon="strength3" ng-show="$ctrl.strength===3"></ias-icon>
+                                <ias-icon class="strength" icon="strength4" ng-show="$ctrl.strength===4"></ias-icon>
+                                <ias-icon class="strength" icon="strength5" ng-show="$ctrl.strength===5"></ias-icon>
+                            </div>
+                        </td>
+                    </tr>
+                    <tr>
+                        <td>
+                            <input ng-model="$ctrl.password2" ng-hide="$ctrl.passwordMasked" type="text">
+                            <input ng-model="$ctrl.password2" ng-show="$ctrl.passwordMasked" type="password">
+                            <div id="password-icon"
+                                 ng-attr-title="{{ 'Button_Show' | translate }}"
+                                 ng-if="$ctrl.maskPasswords">
+                                    <a ng-bind="'Button_Show' | translate"
+                                       ng-click="$ctrl.togglePasswordMasked()"
+                                       tabindex="0"></a>
+                            </div>
+                        </td>
+                        <td>
+                            <ias-icon icon="status_ok_thin"
+                                      ng-attr-title="{{ 'Display_MatchCondition' | translate }}"
+                                      ng-show="$ctrl.matchStatus==='MATCH'"></ias-icon>
+                            <ias-icon icon="message_error_thick"
+                                      ng-attr-title="{{ 'Display_MatchCondition' | translate }}"
+                                      ng-show="$ctrl.matchStatus==='NO_MATCH'"></ias-icon>
+                        </td>
+                    </tr>
+                    </tbody>
+                </table>
+            </div>
+            <div class="ias-actions">
+                <ias-button ng-click="$ctrl.chooseTypedPassword()"
+                            ng-disabled="!$ctrl.passwordAcceptable">{{ 'Button_ChangePassword' | translate }}
+                </ias-button>
+                <ias-button ng-click="$ctrl.onClickRandomPasswords()"
+                            ng-if="$ctrl.passwordUiMode === 'both'">{{ 'Title_RandomPasswords' | translate }}
+                </ias-button>
+            </div>
+
+            <ias-button class="ias-icon-button ias-dialog-cancel-button"
+                        id="close-icon"
+                        ng-attr-title="{{ 'Button_CloseWindow' | translate }}"
+                        ng-click="cancel()">
+                <ias-icon icon="close_thick"></ias-icon>
             </ias-button>
         </div>
-
-        <ias-button class="ias-icon-button ias-dialog-cancel-button"
-                    id="close-icon"
-                    ng-attr-title="{{ 'Button_CloseWindow' | translate }}"
-                    ng-click="cancel()">
-            <ias-icon icon="close_thick"></ias-icon>
-        </ias-button>
     </div>
 </div>

+ 15 - 1
client/src/i18n/translations_en.json

@@ -8,7 +8,10 @@
   "Button_CloseWindow": "Close Window",
   "Button_Confirm": "Confirm",
   "Button_Delete": "Delete",
+  "Button_SendEmail": "Send Email",
+  "Button_Export": "Export",
   "Button_Email": "Email",
+  "Button_ExportOrgChart": "Export Organizational Chart",
   "Button_GoBack": "Go Back",
   "Button_HelpdeskClearOtpSecret": "Clear OTP Secret",
   "Button_More": "More",
@@ -48,6 +51,10 @@
   "Field_Profile": "Profile",
   "Field_Username": "User Name",
   "Long_Title_VerificationSend": "Before this user can be selected, the user's identity must be verified.  Please select a verification method.",
+  "Instructions_ExportOrgChart1": "Click the Export button to begin download of the organizational chart data. After download is complete, you can import the data into a program like Visio so it can be formatted into the desired output.",
+  "Instructions_ExportOrgChart2": "Choose the export level depth, and press the Export button below.",
+  "Instructions_EmailTeam1": "The email list is generated based off of organizational data starting at this point. When you click the Send Email button, your default email program should automatically open, with the list of email addresses pre-filled.",
+  "Instructions_EmailTeam2": "Choose the team level depth, and press the Email button below.",
   "Placeholder_Search": "Search",
   "Title_AdvancedSearch": "Advanced Search",
   "Title_ChangePassword": "Change Password",
@@ -58,6 +65,7 @@
   "Title_Management": "Management",
   "Title_Organization": "Organization",
   "Title_OrgChart": "Organizational Chart",
+  "Title_Print": "Print",
   "Title_PasswordPolicy": "Password Policy",
   "Title_PeopleSearch": "People Search",
   "Title_PeopleSearchCard": "People Search Cards",
@@ -69,5 +77,11 @@
   "Title_Success": "Success",
   "Title_UserEventHistory": "Password History",
   "Title_ValidateCode": "Validate Code",
-  "Title_VerificationSend": "Select verification method"
+  "Title_VerificationSend": "Select verification method",
+  "Title_ExportOrgChart": "Export Organizational Chart",
+  "Title_EmailOrgChart": "Email Team Members",
+  "Label_ExportLevelDepth": "Export Level Depth",
+  "Label_EmailLevelDepth": "Team Level Depth",
+  "Label_EmailTeamMembersFound": "Email Addresses Found",
+  "Label_StartingFrom": "starting from {{personName}}"
 }

+ 40 - 37
client/src/modules/helpdesk/helpdesk-detail-dialog.template.html

@@ -20,48 +20,51 @@
   ~ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
   -->
 
-<div class="ias-dialog" ng-switch="status">
-    <div class="ias-dialog-container" ng-switch-default>
-        <div class="WaitDialogBlank"></div>
-    </div>
 
-    <div class="ias-dialog-container" ng-switch-when="confirm">
-        <div class="ias-dialog-label">
-            <div class="ias-title" ng-bind="title"></div>
-        </div>
-        <div class="ias-dialog-content">
-            <p ng-bind="text"></p>
-            <p ng-bind="secondaryText" ng-if="!!secondaryText"></p>
-        </div>
-        <div class="ias-actions">
-            <ias-button ng-click="confirm()">{{ 'Button_OK' | translate }}</ias-button>
-            <ias-button ng-click="close()">{{ 'Button_Cancel' | translate }}</ias-button>
+<div class="ias-styles-root">
+    <div class="ias-dialog" ng-switch="status">
+        <div class="ias-dialog-container" ng-switch-default>
+            <div class="WaitDialogBlank"></div>
         </div>
 
-        <ias-button class="ias-icon-button ias-dialog-cancel-button"
-                    id="close-icon"
-                    ng-attr-title="{{ 'Button_CloseWindow' | translate }}"
-                    ng-click="close()">
-            <ias-icon icon="close_thick"></ias-icon>
-        </ias-button>
-    </div>
+        <div class="ias-dialog-container" ng-switch-when="confirm">
+            <div class="ias-dialog-label">
+                <div class="ias-title" ng-bind="title"></div>
+            </div>
+            <div class="ias-dialog-content">
+                <p ng-bind="text"></p>
+                <p ng-bind="secondaryText" ng-if="!!secondaryText"></p>
+            </div>
+            <div class="ias-actions">
+                <ias-button ng-click="confirm()">{{ 'Button_OK' | translate }}</ias-button>
+                <ias-button ng-click="close()">{{ 'Button_Cancel' | translate }}</ias-button>
+            </div>
 
-    <div class="ias-dialog-container" ng-switch-when="success">
-        <div class="ias-dialog-label">
-            <div class="ias-title" ng-bind="title"></div>
-        </div>
-        <div class="ias-dialog-content">
-            <p ng-bind="text"></p>
-        </div>
-        <div class="ias-actions">
-            <ias-button ng-click="close()">{{ 'Button_OK' | translate }}</ias-button>
+            <ias-button class="ias-icon-button ias-dialog-cancel-button"
+                        id="close-icon"
+                        ng-attr-title="{{ 'Button_CloseWindow' | translate }}"
+                        ng-click="close()">
+                <ias-icon icon="close_thick"></ias-icon>
+            </ias-button>
         </div>
 
-        <ias-button class="ias-icon-button ias-dialog-cancel-button"
-                    id="close-icon"
-                    ng-attr-title="{{ 'Button_CloseWindow' | translate }}"
-                    ng-click="close()">
-            <ias-icon icon="close_thick"></ias-icon>
-        </ias-button>
+        <div class="ias-dialog-container" ng-switch-when="success">
+            <div class="ias-dialog-label">
+                <div class="ias-title" ng-bind="title"></div>
+            </div>
+            <div class="ias-dialog-content">
+                <p ng-bind="text"></p>
+            </div>
+            <div class="ias-actions">
+                <ias-button ng-click="close()">{{ 'Button_OK' | translate }}</ias-button>
+            </div>
+
+            <ias-button class="ias-icon-button ias-dialog-cancel-button"
+                        id="close-icon"
+                        ng-attr-title="{{ 'Button_CloseWindow' | translate }}"
+                        ng-click="close()">
+                <ias-icon icon="close_thick"></ias-icon>
+            </ias-button>
+        </div>
     </div>
 </div>

+ 160 - 158
client/src/modules/helpdesk/helpdesk-detail.component.scss

@@ -20,244 +20,246 @@
  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
  */
 
-help-desk-detail {
-  display: block;
-  height: 100%;
-  overflow: auto;
-  width: 100%;
-
-  #page-content-title {
-    margin-bottom: 0;
-  }
+.ias-styles-root {
+  help-desk-detail {
+    display: block;
+    height: 100%;
+    overflow: auto;
+    width: 100%;
 
-  > .ias-header {
-    > .help-desk-icons {
-      margin-left: 15px;
+    #page-content-title {
+      margin-bottom: 0;
     }
-  }
-
-  > .help-desk-content {
-    display: flex;
-    flex-flow: row-reverse wrap;
-    justify-content: flex-end;
 
-    > .help-desk-buttons,
-    > .tabset-container {
-      margin-top: 15px;
+    > .ias-header {
+      > .help-desk-icons {
+        margin-left: 15px;
+      }
     }
 
-    > .help-desk-buttons {
-      > .ias-button {
-        display: block;
-        margin-bottom: 5px;
-        min-width: 170px;
-        width: 100%;
+    > .help-desk-content {
+      display: flex;
+      flex-flow: row-reverse wrap;
+      justify-content: flex-end;
+
+      > .help-desk-buttons,
+      > .tabset-container {
+        margin-top: 15px;
+      }
 
-        &:last-child {
-          margin-bottom: 0;
+      > .help-desk-buttons {
+        > .ias-button {
+          display: block;
+          margin-bottom: 5px;
+          min-width: 170px;
+          width: 100%;
+
+          &:last-child {
+            margin-bottom: 0;
+          }
         }
       }
-    }
 
-    > .tabset-container {
-      width: 100%;
+      > .tabset-container {
+        width: 100%;
 
-      > .tab-panes {
-        > .ias-tab-pane {
-          table {
-            width: 100%;
-            display: block;
-            overflow: auto;
+        > .tab-panes {
+          > .ias-tab-pane {
+            table {
+              width: 100%;
+              display: block;
+              overflow: auto;
+            }
           }
         }
       }
     }
-  }
-
-  .details-table {
-    border: none;
-    border-collapse: collapse;
 
-    tr {
-      height: 25px;
+    .details-table {
+      border: none;
+      border-collapse: collapse;
 
-      td {
-        border: none;
-        font-size: 12px;
-        height: 19px;
-        text-align: left;
+      tr {
+        height: 25px;
 
-        &:first-child {
-          color: #949494;
-          text-align: right;
-          padding: 3px 0;
-        }
+        td {
+          border: none;
+          font-size: 12px;
+          height: 19px;
+          text-align: left;
 
-        &:last-child {
-          padding: 3px 15px;
-        }
+          &:first-child {
+            color: #949494;
+            text-align: right;
+            padding: 3px 0;
+          }
 
-        ul {
-          list-style: none;
-          margin: 0;
-          padding: 0;
+          &:last-child {
+            padding: 3px 15px;
+          }
 
-          > li {
+          ul {
+            list-style: none;
             margin: 0;
             padding: 0;
+
+            > li {
+              margin: 0;
+              padding: 0;
+            }
           }
         }
       }
     }
   }
-}
 
-@media (min-width: 850px) {
-  help-desk-detail {
-    > .help-desk-content {
-      > .help-desk-buttons {
-        margin-left: 15px;
-      }
+  @media (min-width: 850px) {
+    help-desk-detail {
+      > .help-desk-content {
+        > .help-desk-buttons {
+          margin-left: 15px;
+        }
 
-      > .tabset-container {
-        max-width: 100%;
-        width: auto;
+        > .tabset-container {
+          max-width: 100%;
+          width: auto;
 
-        > .tab-panes {
-          > .ias-tab-pane {
-            table {
-              max-width: 800px;
+          > .tab-panes {
+            > .ias-tab-pane {
+              table {
+                max-width: 800px;
+              }
             }
           }
         }
       }
     }
   }
-}
-
-@media (min-width: 1050px) {
-  help-desk-detail {
-    display: flex;
-    flex-flow: column nowrap;
-
-    > .ias-header,
-    > .secondary-header {
-      flex-shrink: 0;
-    }
 
-    > .help-desk-content {
-      flex: 1 1px;
-      flex-wrap: nowrap;
-      height: 100%;
-      margin-top: 15px;
-      overflow: auto;
-
-      > .help-desk-buttons,
-      > .tabset-container {
-        margin-top: 0;
-      }
+  @media (min-width: 1050px) {
+    help-desk-detail {
+      display: flex;
+      flex-flow: column nowrap;
 
-      > .help-desk-buttons {
+      > .ias-header,
+      > .secondary-header {
         flex-shrink: 0;
-        height: 100%;
-        overflow: auto;
-        padding-right: 17px;
       }
 
-      > .tabset-container {
-        display: flex;
-        flex-flow: column nowrap;
+      > .help-desk-content {
+        flex: 1 1px;
+        flex-wrap: nowrap;
         height: 100%;
+        margin-top: 15px;
         overflow: auto;
 
-        > .ias-tabset {
+        > .help-desk-buttons,
+        > .tabset-container {
+          margin-top: 0;
+        }
+
+        > .help-desk-buttons {
           flex-shrink: 0;
+          height: 100%;
+          overflow: auto;
+          padding-right: 17px;
         }
 
-        .tab-panes {
-          flex: 1 1px;
+        > .tabset-container {
+          display: flex;
+          flex-flow: column nowrap;
+          height: 100%;
           overflow: auto;
 
-          > .ias-tab-pane {
-            table {
-              height: 100%;
-            }
+          > .ias-tabset {
+            flex-shrink: 0;
           }
-        }
-      }
-    }
-  }
-}
 
-[dir="rtl"] {
-  help-desk-detail {
-    > .ias-header {
-      > .help-desk-icons {
-        margin-left: 0;
-        margin-right: 15px;
-      }
-    }
-
-    .details-table {
-      tr {
-        td {
-          text-align: right;
+          .tab-panes {
+            flex: 1 1px;
+            overflow: auto;
 
-          &:first-child {
-            text-align: left;
+            > .ias-tab-pane {
+              table {
+                height: 100%;
+              }
+            }
           }
         }
       }
     }
   }
 
-  @media (min-width: 850px) {
+  [dir="rtl"] {
     help-desk-detail {
-      > .help-desk-content {
-        > .help-desk-buttons {
+      > .ias-header {
+        > .help-desk-icons {
           margin-left: 0;
           margin-right: 15px;
         }
       }
+
+      .details-table {
+        tr {
+          td {
+            text-align: right;
+
+            &:first-child {
+              text-align: left;
+            }
+          }
+        }
+      }
     }
-  }
 
-  @media (min-width: 1050px) {
-    help-desk-detail {
-      > .help-desk-content {
-        > .help-desk-buttons {
-          padding-left: 17px;
-          padding-right: 0;
+    @media (min-width: 850px) {
+      help-desk-detail {
+        > .help-desk-content {
+          > .help-desk-buttons {
+            margin-left: 0;
+            margin-right: 15px;
+          }
         }
       }
     }
-  }
-}
 
-// Unlike IE, Edge, and Firefox, Chrome and Safari do not need extra space for the button scrollbar when the buttons
-// extend below the bottom of the page
-@media (min-width: 1050px) {
-  [data-browsertype=webkit] {
-    help-desk-detail {
-      > .help-desk-content {
-        > .help-desk-buttons {
-          padding-right: 0;
+    @media (min-width: 1050px) {
+      help-desk-detail {
+        > .help-desk-content {
+          > .help-desk-buttons {
+            padding-left: 17px;
+            padding-right: 0;
+          }
         }
       }
     }
+  }
 
-    &[dir="rtl"] {
+  // Unlike IE, Edge, and Firefox, Chrome and Safari do not need extra space for the button scrollbar when the buttons
+  // extend below the bottom of the page
+  @media (min-width: 1050px) {
+    [data-browsertype=webkit] {
       help-desk-detail {
         > .help-desk-content {
           > .help-desk-buttons {
-            padding-left: 0;
+            padding-right: 0;
+          }
+        }
+      }
+
+      &[dir="rtl"] {
+        help-desk-detail {
+          > .help-desk-content {
+            > .help-desk-buttons {
+              padding-left: 0;
+            }
           }
         }
       }
     }
   }
-}
 
-.ias-tab-pane.ias-open {
-  display: block;   // Can remove once https://github.com/MicroFocus/ux-ias/issues/18 (2nd comment) is resolved
+  .ias-tab-pane.ias-open {
+    display: block; // Can remove once https://github.com/MicroFocus/ux-ias/issues/18 (2nd comment) is resolved
+  }
 }

+ 66 - 65
client/src/modules/helpdesk/helpdesk-search.component.scss

@@ -20,93 +20,94 @@
  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
  */
 
-
-help-desk-search-cards,
-help-desk-search-table {
-  display: flex;
-  flex-flow: column nowrap;
-  height: 100%;
-
-  #page-content-title {
-    margin-bottom: 0;
-  }
-
-  .verifications-button {
-    margin: 5px 5px 5px 0;
-  }
-
-  > .people-search-component-content {
-    flex: 1 1;
-    overflow: auto;
-  }
-
-  .helpdesk-search-header {
+.ias-styles-root {
+  help-desk-search-cards,
+  help-desk-search-table {
     display: flex;
-    align-items: flex-start;
+    flex-flow: column nowrap;
+    height: 100%;
 
-    .basic-search-container {
-      display: flex;
-      align-items: center;
-      margin-bottom: 15px;
+    #page-content-title {
+      margin-bottom: 0;
+    }
 
-      > * + * {
-        margin-left: 10px;
-      }
+    .verifications-button {
+      margin: 5px 5px 5px 0;
     }
 
-    .advanced-search-container {
+    > .people-search-component-content {
+      flex: 1 1;
+      overflow: auto;
+    }
+
+    .helpdesk-search-header {
       display: flex;
-      flex-direction: column;
       align-items: flex-start;
-      margin-bottom: 15px;
 
-      > * + * {
-        margin-top: 5px;
+      .basic-search-container {
+        display: flex;
+        align-items: center;
+        margin-bottom: 15px;
+
+        > * + * {
+          margin-left: 10px;
+        }
       }
 
-      &+ div {
-        margin-top: 15px;
+      .advanced-search-container {
+        display: flex;
+        flex-direction: column;
+        align-items: flex-start;
+        margin-bottom: 15px;
+
+        > * + * {
+          margin-top: 5px;
+        }
+
+        & + div {
+          margin-top: 15px;
+        }
       }
     }
   }
-}
 
-.aligned-input {
-  margin-top: 15px;
+  .aligned-input {
+    margin-top: 15px;
 
-  > * {
-    vertical-align: middle;
-  }
+    > * {
+      vertical-align: middle;
+    }
 
-  .ias-button {
-    margin-right: 5px;
+    .ias-button {
+      margin-right: 5px;
+    }
   }
-}
 
-.loading-gif-25 {
-  background-image: url('../../../images/icons/wait_25.gif');
-  display: inline-block;
-  height: 25px;
-  width: 25px;
-}
+  .loading-gif-25 {
+    background-image: url('../../../images/icons/wait_25.gif');
+    display: inline-block;
+    height: 25px;
+    width: 25px;
+  }
 
-[dir="rtl"] {
-  help-desk-search-cards,
-  help-desk-search-table {
-    .ias-search {
-      margin-left: 10px;
-      margin-right: 0;
-    }
+  [dir="rtl"] {
+    help-desk-search-cards,
+    help-desk-search-table {
+      .ias-search {
+        margin-left: 10px;
+        margin-right: 0;
+      }
 
-    .verifications-button {
-      margin: 5px 0 5px 5px;
+      .verifications-button {
+        margin: 5px 0 5px 5px;
+      }
     }
-  }
 
-  .aligned-input {
-    .ias-button {
-      margin-left: 5px;
-      margin-right: 0;
+    .aligned-input {
+      .ias-button {
+        margin-left: 5px;
+        margin-right: 0;
+      }
     }
   }
 }

+ 4 - 2
client/src/modules/helpdesk/helpdesk.module.ts

@@ -20,6 +20,10 @@
  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
  */
 
+// These need to be at the top so imported components can override the default styling
+require('../../styles.scss');
+require('../peoplesearch/peoplesearch.scss');
+
 import 'angular-aria';
 
 import {IComponentOptions, module} from 'angular';
@@ -40,8 +44,6 @@ import SuccessChangePasswordController from '../../components/changepassword/suc
 import TypeChangePasswordController from '../../components/changepassword/type-change-password.controller';
 import CommonSearchService from '../../services/common-search.service';
 
-require('../peoplesearch/peoplesearch.scss');
-
 const moduleName = 'help-desk';
 
 module(moduleName, [

+ 42 - 40
client/src/modules/helpdesk/recent-verifications-dialog.template.html

@@ -20,49 +20,51 @@
   ~ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
   -->
 
-<div class="ias-dialog">
-    <div class="ias-dialog-container">
-        <div class="ias-dialog-label">
-            <div class="ias-title" ng-bind="'Title_RecentVerifications' | translate"></div>
-        </div>
+<div class="ias-styles-root">
+    <div class="ias-dialog">
+        <div class="ias-dialog-container">
+            <div class="ias-dialog-label">
+                <div class="ias-title" ng-bind="'Title_RecentVerifications' | translate"></div>
+            </div>
 
-        <div class="ias-dialog-content">
-            <p ng-bind="'Display_SearchResultsNone' | translate"
-               ng-if="!$ctrl.recentVerifications.length"></p>
-            <table class="ias-table" ng-if="!!$ctrl.recentVerifications.length">
-                <thead>
-                <tr>
-                    <th ng-bind="'Field_LdapProfile' | translate"></th>
-                    <th ng-bind="'Field_Username' | translate"></th>
-                    <th ng-bind="'Field_DateTime' | translate"></th>
-                    <th ng-bind="'Field_Method' | translate"></th>
-                </tr>
-                </thead>
-                <tbody>
-                <tr ng-repeat="record in $ctrl.recentVerifications">
-                    <td ng-bind="record.profile"></td>
-                    <td ng-bind="record.username"></td>
-                    <td ng-bind="record.timestamp | dateFilter"></td>
-                    <td ng-bind="record.method"></td>
-                </tr>
-                </tbody>
-            </table>
-            <div ng-repeat="method in $ctrl.availableVerificationMethods">
-                <ias-button ng-click="$ctrl.selectVerificationMethod(method.name)">
-                    {{ method.label | translate }}
-                </ias-button>
+            <div class="ias-dialog-content">
+                <p ng-bind="'Display_SearchResultsNone' | translate"
+                   ng-if="!$ctrl.recentVerifications.length"></p>
+                <table class="ias-table" ng-if="!!$ctrl.recentVerifications.length">
+                    <thead>
+                    <tr>
+                        <th ng-bind="'Field_LdapProfile' | translate"></th>
+                        <th ng-bind="'Field_Username' | translate"></th>
+                        <th ng-bind="'Field_DateTime' | translate"></th>
+                        <th ng-bind="'Field_Method' | translate"></th>
+                    </tr>
+                    </thead>
+                    <tbody>
+                    <tr ng-repeat="record in $ctrl.recentVerifications">
+                        <td ng-bind="record.profile"></td>
+                        <td ng-bind="record.username"></td>
+                        <td ng-bind="record.timestamp | dateFilter"></td>
+                        <td ng-bind="record.method"></td>
+                    </tr>
+                    </tbody>
+                </table>
+                <div ng-repeat="method in $ctrl.availableVerificationMethods">
+                    <ias-button ng-click="$ctrl.selectVerificationMethod(method.name)">
+                        {{ method.label | translate }}
+                    </ias-button>
+                </div>
             </div>
-        </div>
 
-        <div class="ias-actions">
-            <ias-button ng-click="close()">{{ 'Button_OK' | translate }}</ias-button>
-        </div>
+            <div class="ias-actions">
+                <ias-button ng-click="close()">{{ 'Button_OK' | translate }}</ias-button>
+            </div>
 
-        <ias-button class="ias-icon-button ias-dialog-cancel-button"
-                    id="close-icon"
-                    ng-attr-title="{{ 'Button_CloseWindow' | translate }}"
-                    ng-click="close()">
-            <ias-icon icon="close_thick"></ias-icon>
-        </ias-button>
+            <ias-button class="ias-icon-button ias-dialog-cancel-button"
+                        id="close-icon"
+                        ng-attr-title="{{ 'Button_CloseWindow' | translate }}"
+                        ng-click="close()">
+                <ias-icon icon="close_thick"></ias-icon>
+            </ias-button>
+        </div>
     </div>
 </div>

+ 83 - 81
client/src/modules/helpdesk/verifications-dialog.template.html

@@ -20,99 +20,101 @@
   ~ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
   -->
 
-<div class="ias-dialog" ng-switch="$ctrl.status">
-        <div class="ias-dialog-container" ng-switch-when="select">
-            <div class="ias-dialog-label">
-                <div class="ias-title" ng-bind="'Title_VerificationSend' | translate"></div>
-            </div>
-            <div class="ias-dialog-content">
-                <p ng-bind="'Long_Title_VerificationSend' | translate"></p>
-
-                <ias-button ng-click="$ctrl.selectVerificationMethod(method.name)"
-                            ng-repeat="method in $ctrl.availableVerificationMethods"
-                            style="margin-right: 3px;">
-                    {{ method.label | translate }}
-                </ias-button>
+<div class="ias-styles-root">
+    <div class="ias-dialog" ng-switch="$ctrl.status">
+            <div class="ias-dialog-container" ng-switch-when="select">
+                <div class="ias-dialog-label">
+                    <div class="ias-title" ng-bind="'Title_VerificationSend' | translate"></div>
+                </div>
+                <div class="ias-dialog-content">
+                    <p ng-bind="'Long_Title_VerificationSend' | translate"></p>
 
-            </div>
-            <div class="ias-actions">
-                <ias-button ng-click="close()">{{ 'Button_Cancel' | translate }}</ias-button>
-            </div>
+                    <ias-button ng-click="$ctrl.selectVerificationMethod(method.name)"
+                                ng-repeat="method in $ctrl.availableVerificationMethods"
+                                style="margin-right: 3px;">
+                        {{ method.label | translate }}
+                    </ias-button>
 
-            <ias-button class="ias-icon-button ias-dialog-cancel-button"
-                        id="close-icon"
-                        ng-attr-title="{{ 'Button_CloseWindow' | translate }}"
-                        ng-click="close()">
-                <ias-icon icon="close_thick"></ias-icon>
-            </ias-button>
-        </div>
+                </div>
+                <div class="ias-actions">
+                    <ias-button ng-click="close()">{{ 'Button_Cancel' | translate }}</ias-button>
+                </div>
 
-        <div class="ias-dialog-container" ng-switch-when="verify">
-            <div class="ias-dialog-label">
-                <div class="ias-title" ng-bind="'Title_ValidateCode' | translate"></div>
+                <ias-button class="ias-icon-button ias-dialog-cancel-button"
+                            id="close-icon"
+                            ng-attr-title="{{ 'Button_CloseWindow' | translate }}"
+                            ng-click="close()">
+                    <ias-icon icon="close_thick"></ias-icon>
+                </ias-button>
             </div>
 
-            <div class="ias-dialog-content" ng-switch="$ctrl.verificationMethod">
-                <form>
-                    <div ng-switch-when="ATTRIBUTES">
-                        <div class="ias-input-container" ng-repeat="input in $ctrl.inputs">
-                            <label ng-attr-for="{{input.name}}" ng-bind="input.label"></label>
-                            <input autofocus
-                                   autocomplete="off"
-                                   ng-attr-id="{{input.name}}"
-                                   type="text"
-                                   ng-model="$ctrl.formData[input.name]">
+            <div class="ias-dialog-container" ng-switch-when="verify">
+                <div class="ias-dialog-label">
+                    <div class="ias-title" ng-bind="'Title_ValidateCode' | translate"></div>
+                </div>
+
+                <div class="ias-dialog-content" ng-switch="$ctrl.verificationMethod">
+                    <form>
+                        <div ng-switch-when="ATTRIBUTES">
+                            <div class="ias-input-container" ng-repeat="input in $ctrl.inputs">
+                                <label ng-attr-for="{{input.name}}" ng-bind="input.label"></label>
+                                <input autofocus
+                                       autocomplete="off"
+                                       ng-attr-id="{{input.name}}"
+                                       type="text"
+                                       ng-model="$ctrl.formData[input.name]">
+                            </div>
                         </div>
-                    </div>
-                    <div ng-switch-when="EMAIL|SMS" ng-switch-when-separator="|">
-                        <div class="ias-input-container">
-                            <label ng-bind="'Display_TokenDestination' | translate"></label>
-                            <input type="text" ng-value="$ctrl.tokenData.destination" readonly>
+                        <div ng-switch-when="EMAIL|SMS" ng-switch-when-separator="|">
+                            <div class="ias-input-container">
+                                <label ng-bind="'Display_TokenDestination' | translate"></label>
+                                <input type="text" ng-value="$ctrl.tokenData.destination" readonly>
+                            </div>
+                            <div class="ias-input-container">
+                                <label>Token</label>
+                                <input autofocus autocomplete="off" id="token" type="text" ng-model="$ctrl.formData.code">
+                            </div>
+
                         </div>
-                        <div class="ias-input-container">
-                            <label>Token</label>
-                            <input autofocus autocomplete="off" id="token" type="text" ng-model="$ctrl.formData.code">
+                        <div ng-switch-when="OTP">
+                            <p ng-bind="'Display_HelpdeskOtpValidation' | translate"></p>
+                            <div class="ias-input-container">
+                                <label>Code</label>
+                                <input autofocus autocomplete="off" id="code" type="text" ng-model="$ctrl.formData.code">
+                            </div>
                         </div>
 
-                    </div>
-                    <div ng-switch-when="OTP">
-                        <p ng-bind="'Display_HelpdeskOtpValidation' | translate"></p>
-                        <div class="ias-input-container">
-                            <label>Code</label>
-                            <input autofocus autocomplete="off" id="code" type="text" ng-model="$ctrl.formData.code">
+                        <div class="aligned-input">
+                            <ias-button ng-click="$ctrl.sendVerificationData()" ng-disabled="$ctrl.verificationStatus==='passed'">
+                                {{ 'Button_Verify' | translate }}
+                            </ias-button>
+                            <div class="loading-gif-25" ng-if="$ctrl.verificationStatus==='wait'"></div>
+                            <ias-icon icon="status_ok_thick" style="color:#37c26a;" class="ias-success" ng-if="$ctrl.verificationStatus==='passed'"></ias-icon>
+                            <ias-icon icon="status_error_thick" style="color:#e50000;" class="ias-error" ng-if="$ctrl.verificationStatus==='failed'"></ias-icon>
                         </div>
-                    </div>
+                        <p ng-bind="'Display_InvalidVerification' | translate" ng-if="$ctrl.verificationStatus==='failed'"></p>
+                    </form>
+                </div>
 
-                    <div class="aligned-input">
-                        <ias-button ng-click="$ctrl.sendVerificationData()" ng-disabled="$ctrl.verificationStatus==='passed'">
-                            {{ 'Button_Verify' | translate }}
-                        </ias-button>
-                        <div class="loading-gif-25" ng-if="$ctrl.verificationStatus==='wait'"></div>
-                        <ias-icon icon="status_ok_thick" style="color:#37c26a;" class="ias-success" ng-if="$ctrl.verificationStatus==='passed'"></ias-icon>
-                        <ias-icon icon="status_error_thick" style="color:#e50000;" class="ias-error" ng-if="$ctrl.verificationStatus==='failed'"></ias-icon>
-                    </div>
-                    <p ng-bind="'Display_InvalidVerification' | translate" ng-if="$ctrl.verificationStatus==='failed'"></p>
-                </form>
-            </div>
+                <div class="ias-actions">
+                    <ias-button ng-bind="'Display_ViewDetails' | translate"
+                                ng-disabled="$ctrl.verificationStatus!=='passed'"
+                                ng-click="$ctrl.viewDetails()"
+                                ng-if="!$ctrl.isDetailsView"></ias-button>
+                    <ias-button ng-disabled="$ctrl.verificationStatus!=='passed'"
+                               ng-click="$ctrl.clickOkButton()"
+                               ng-if="$ctrl.isDetailsView">
+                        {{ 'Button_OK' | translate }}
+                    </ias-button>
+                    <ias-button ng-click="close()">{{ 'Button_Cancel' | translate }}</ias-button>
+                </div>
 
-            <div class="ias-actions">
-                <ias-button ng-bind="'Display_ViewDetails' | translate"
-                            ng-disabled="$ctrl.verificationStatus!=='passed'"
-                            ng-click="$ctrl.viewDetails()"
-                            ng-if="!$ctrl.isDetailsView"></ias-button>
-                <ias-button ng-disabled="$ctrl.verificationStatus!=='passed'"
-                           ng-click="$ctrl.clickOkButton()"
-                           ng-if="$ctrl.isDetailsView">
-                    {{ 'Button_OK' | translate }}
+                <ias-button class="ias-icon-button ias-dialog-cancel-button"
+                            id="close-icon"
+                            ng-attr-title="{{ 'Button_CloseWindow' | translate }}"
+                            ng-click="close()">
+                    <ias-icon icon="close_thick"></ias-icon>
                 </ias-button>
-                <ias-button ng-click="close()">{{ 'Button_Cancel' | translate }}</ias-button>
             </div>
-
-            <ias-button class="ias-icon-button ias-dialog-cancel-button"
-                        id="close-icon"
-                        ng-attr-title="{{ 'Button_CloseWindow' | translate }}"
-                        ng-click="close()">
-                <ias-icon icon="close_thick"></ias-icon>
-            </ias-button>
-        </div>
+    </div>
 </div>

+ 54 - 0
client/src/modules/peoplesearch/orgchart-email.component.html

@@ -0,0 +1,54 @@
+<!--
+  ~ Password Management Servlets (PWM)
+  ~ http://www.pwm-project.org
+  ~
+  ~ Copyright (c) 2006-2009 Novell, Inc.
+  ~ Copyright (c) 2009-2018 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
+  -->
+
+<div class="ias-styles-root">
+    <div class="ias-dialog">
+        <div class="ias-dialog-container">
+            <div class="ias-dialog-label">
+                <div class="ias-title" ng-bind="'Title_EmailOrgChart' | translate"></div>
+            </div>
+            <div class="ias-dialog-content">
+                <p ng-bind="'Instructions_EmailTeam1' | translate"></p>
+                <div ng-if="$ctrl.maxDepth > 1" class="ias-input-container">
+                    <label for="depth" ng-bind="'Label_EmailLevelDepth' | translate"></label>
+                    <select id="depth" ng-model="$ctrl.depth" ng-change="$ctrl.depthChanged()">
+                        <option ng-repeat="x in [].constructor($ctrl.maxDepth) track by $index">{{$index+1}}</option>
+                    </select>
+                    {{ 'Label_StartingFrom' | translate: { personName: $ctrl.personName } }}
+                </div>
+                <div id="teamMembersContainer" class="ias-input-container">
+                    <label for="teamMembers" ng-bind="'Label_EmailTeamMembersFound' | translate"></label>
+                    <textarea id="teamMembers" ng-model="$ctrl.teamEmailList"></textarea>
+                </div>
+            </div>
+            <div class="ias-actions">
+                <ias-button ng-disabled="$ctrl.fetchingTeamMembers" ng-click="$ctrl.emailOrgChart()">{{ 'Button_SendEmail' | translate }}</ias-button>
+            </div>
+            <ias-button class="ias-icon-button ias-dialog-cancel-button"
+                        id="close-icon"
+                        ng-attr-title="{{ 'Button_CloseWindow' | translate }}"
+                        ng-click="close()">
+                <ias-icon icon="close_thick"></ias-icon>
+            </ias-button>
+        </div>
+    </div>
+</div>

+ 12 - 0
client/src/modules/peoplesearch/orgchart-email.component.scss

@@ -0,0 +1,12 @@
+.ias-styles-root {
+    #teamMembersContainer {
+        display: flex;
+        flex-direction: column;
+        height: 150px;
+
+        > textarea {
+            align-self: stretch;
+            flex-grow: 1;
+        }
+    }
+}

+ 73 - 0
client/src/modules/peoplesearch/orgchart-email.controller.ts

@@ -0,0 +1,73 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2018 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 {IPeopleService} from '../../services/people.service';
+
+require('./orgchart-email.component.scss');
+
+export default class OrgchartEmailController {
+    depth = '1';
+    fetchingTeamMembers = false;
+    teamEmailList: string;
+
+    static $inject = [
+        '$window',
+        'IasDialogService',
+        'translateFilter',
+        'peopleService',
+        'maxDepth',
+        'personName',
+        'userKey'
+    ];
+    constructor(private $window: angular.IWindowService,
+                private IasDialogService: any,
+                private translateFilter: (id: string) => string,
+                private peopleService: IPeopleService,
+                private maxDepth: number,
+                private personName: string,
+                private userKey: string) {
+
+        this.fetchEmailList();
+    }
+
+    emailOrgChart() {
+        this.$window.location.href = `mailto:${this.teamEmailList}`;
+        this.IasDialogService.close();
+    }
+
+    depthChanged() {
+        this.fetchEmailList();
+    }
+
+    fetchEmailList() {
+        this.fetchingTeamMembers = true;
+
+        this.peopleService.getTeamEmails(this.userKey, +this.depth)
+            .then((teamEmails: string[]) => {
+                this.teamEmailList = teamEmails.toString();
+            })
+            .finally(() => {
+                this.fetchingTeamMembers = false;
+            });
+    }
+}

+ 50 - 0
client/src/modules/peoplesearch/orgchart-export.component.html

@@ -0,0 +1,50 @@
+<!--
+  ~ Password Management Servlets (PWM)
+  ~ http://www.pwm-project.org
+  ~
+  ~ Copyright (c) 2006-2009 Novell, Inc.
+  ~ Copyright (c) 2009-2018 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
+  -->
+
+<div class="ias-styles-root">
+    <div class="ias-dialog">
+        <div class="ias-dialog-container">
+            <div class="ias-dialog-label">
+                <div class="ias-title" ng-bind="'Title_ExportOrgChart' | translate"></div>
+            </div>
+            <div class="ias-dialog-content">
+                <p ng-bind="'Instructions_ExportOrgChart1' | translate"></p>
+                <label ng-if="$ctrl.maxDepth > 1" for="depth" ng-bind="'Label_ExportLevelDepth' | translate"></label>
+                <div ng-if="$ctrl.maxDepth > 1">
+                    <select id="depth" ng-model="$ctrl.depth">
+                        <option ng-repeat="x in [].constructor($ctrl.maxDepth) track by $index">{{$index+1}}</option>
+                    </select>
+                    {{ 'Label_StartingFrom' | translate: { personName: $ctrl.personName } }}
+                </div>
+            </div>
+            <div class="ias-actions">
+                <ias-button ng-click="$ctrl.exportOrgChart()">{{ 'Button_Export' | translate }}</ias-button>
+            </div>
+            <ias-button class="ias-icon-button ias-dialog-cancel-button"
+                        id="close-icon"
+                        ng-attr-title="{{ 'Button_CloseWindow' | translate }}"
+                        ng-click="close()">
+                <ias-icon icon="close_thick"></ias-icon>
+            </ias-button>
+        </div>
+    </div>
+</div>

+ 2 - 0
client/src/modules/peoplesearch/orgchart-export.component.scss

@@ -0,0 +1,2 @@
+.ias-styles-root {
+}

+ 53 - 0
client/src/modules/peoplesearch/orgchart-export.controller.ts

@@ -0,0 +1,53 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2018 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';
+
+require('./orgchart-export.component.scss');
+
+export default class OrgchartExportController {
+    depth = '1';
+
+    static $inject = [
+        '$window',
+        'IasDialogService',
+        'translateFilter',
+        'peopleService',
+        'maxDepth',
+        'personName',
+        'userKey'
+    ];
+    constructor(private $window: angular.IWindowService,
+                private IasDialogService: any,
+                private translateFilter: (id: string) => string,
+                private peopleService: IPeopleService,
+                private maxDepth: number,
+                private personName: string,
+                private userKey: string) {
+    }
+
+    exportOrgChart() {
+        // tslint:disable-next-line
+        this.$window.location.href = `/pwm/private/peoplesearch?processAction=exportOrgChart&depth=${this.depth}&userKey=${this.userKey}`;
+        this.IasDialogService.close();
+    }
+}

+ 6 - 1
client/src/modules/peoplesearch/orgchart-search.component.html

@@ -47,6 +47,11 @@
                 ng-attr-title="{{ 'Title_OrgChart' | translate }}">
         <ias-icon class="ias-selected" icon="orgchart_thin"></ias-icon>
     </ias-button>
+    <ias-button id="print-icon" class="ias-icon-button"
+                ng-click="$ctrl.printOrgChart()" ng-if="$ctrl.printEnabled"
+                ng-attr-title="{{ 'Title_Print' | translate }}">
+        <ias-icon class="ias-selected" icon="printer"></ias-icon>
+    </ias-button>
 </div>
 
 
@@ -57,4 +62,4 @@
            management-chain="$ctrl.managementChain">
 </org-chart>
 
-<ui-view></ui-view>
+<ui-view></ui-view>

+ 12 - 11
client/src/modules/peoplesearch/orgchart-search.component.scss

@@ -20,18 +20,19 @@
  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
  */
 
+.ias-styles-root {
+  org-chart-search {
+    #page-content-title {
+      margin-bottom: 0;
+    }
 
-org-chart-search {
-  #page-content-title {
-    margin-bottom: 0;
-  }
-
-  display: flex;
-  flex-flow: column nowrap;
-  height: 100%;
+    display: flex;
+    flex-flow: column nowrap;
+    height: 100%;
 
-  > org-chart {
-    flex: 1 1;
-    overflow-x: auto;
+    > org-chart {
+      flex: 1 1;
+      overflow-x: auto;
+    }
   }
 }

+ 13 - 1
client/src/modules/peoplesearch/orgchart-search.component.ts

@@ -25,7 +25,7 @@ import { Component } from '../../component';
 import { IPeopleSearchConfigService } from '../../services/peoplesearch-config.service';
 import { IPeopleService } from '../../services/people.service';
 import IPwmService from '../../services/pwm.service';
-import {isArray, isString, IPromise, IQService, IScope, ITimeoutService} from 'angular';
+import {isArray, isString, IPromise, IQService, IScope, ITimeoutService, IWindowService} from 'angular';
 import LocalStorageService from '../../services/local-storage.service';
 import IOrgChartData from '../../models/orgchart-data.model';
 import { IPerson } from '../../models/person.model';
@@ -44,11 +44,13 @@ export default class OrgChartSearchComponent {
     managementChainLimit: number;
     query: string;
     searchTextLocalStorageKey: string;
+    printEnabled: boolean;
 
     static $inject = [
         '$state',
         '$stateParams',
         '$timeout',
+        '$window',
         'ConfigService',
         'LocalStorageService',
         'PeopleService',
@@ -57,6 +59,7 @@ export default class OrgChartSearchComponent {
     constructor(private $state: angular.ui.IStateService,
                 private $stateParams: angular.ui.IStateParamsService,
                 private $timeout: ITimeoutService,
+                private $window: IWindowService,
                 private configService: IPeopleSearchConfigService,
                 private localStorageService: LocalStorageService,
                 private peopleService: IPeopleService,
@@ -78,6 +81,11 @@ export default class OrgChartSearchComponent {
                 this.managementChainLimit = orgChartMaxParents;
             });
 
+        this.configService.printingEnabled().then(
+            (printingEnabled: boolean) => {
+                this.printEnabled = printingEnabled;
+            });
+
         this.query = this.getSearchText();
 
         let personId: string = this.$stateParams['personId'];
@@ -163,4 +171,8 @@ export default class OrgChartSearchComponent {
     protected storeSearchText(query): void {
         this.localStorageService.setItem(this.searchTextLocalStorageKey, query || '');
     }
+
+    private printOrgChart(): void {
+        this.$window.print();
+    }
 }

+ 285 - 284
client/src/modules/peoplesearch/orgchart.component.scss

@@ -27,401 +27,402 @@ $org-chart-text-color: #808080;
 
 $manager-connector-height: 16px;
 
-.reports {
-  background-color: #dae1e1;
-  border-radius: 2px;
-  color: #434c50;
-  font-size: 14px;
-  height: 25px;
-  line-height: 25px;
-  position: absolute;
-  right: 3px;
-  text-align: center;
-  top: 3px;
-  min-width: 35px;
-}
-
-.assistant,
-.manager {
-  .ias-tile {
-    border: none;
-    background-color: transparent;
-    display: block;
-    height: 96px;
-    padding: 0;
-    vertical-align: top;
-    width: 120px;
+.ias-styles-root {
+  .reports {
+    background-color: #dae1e1;
+    border-radius: 2px;
+    color: #434c50;
+    font-size: 14px;
+    height: 25px;
+    line-height: 25px;
+    position: absolute;
+    right: 3px;
+    text-align: center;
+    top: 3px;
+    min-width: 35px;
+  }
 
-    > .ias-avatar {
-      border: 3px solid #808080;
-      border-radius: 100%;
+  .assistant,
+  .manager {
+    .ias-tile {
+      border: none;
+      background-color: transparent;
       display: block;
-      margin: 0 auto;
+      height: 96px;
+      padding: 0;
+      vertical-align: top;
+      width: 120px;
+
+      > .ias-avatar {
+        border: 3px solid #808080;
+        border-radius: 100%;
+        display: block;
+        margin: 0 auto;
 
-      &:hover {
-        //border-color: $person-card-border-color;
+        &:hover {
+          //border-color: $person-card-border-color;
+        }
       }
-    }
 
-    > .reports {
-      right: 20px;
-    }
-
-    > .ias-tile-content {
-      background-color: white;
-      display: block;
-      margin-top: 8px;
-      text-align: center;
-      width: 100%;
-
-      :nth-child(n + 3) {
-        display: none;
+      > .reports {
+        right: 20px;
       }
-    }
-  }
 
-  .reports {
-    right: 20px;
-  }
-}
-
-.self {
-  &.ias-tile {
-    background-color: #ffffff;
-    border: 3px solid #808080;
-    border-radius: 3px;
-    height: auto;
-    min-height: 96px;
-    //width: 346px;
-    max-width: 100%;
+      > .ias-tile-content {
+        background-color: white;
+        display: block;
+        margin-top: 8px;
+        text-align: center;
+        width: 100%;
 
-    > .ias-avatar {
-      flex: 0 0 65px;
-      height: 65px;
-      width: 65px;
-      margin-bottom: 5px;
+        :nth-child(n + 3) {
+          display: none;
+        }
+      }
     }
 
-    > .ias-tile-content {
-      flex-flow: row nowrap;
+    .reports {
+      right: 20px;
     }
   }
-}
-
 
-// (XS) Default display
-org-chart {
-  display: block;
-  max-width: 100%;
+  .self {
+    &.ias-tile {
+      background-color: #ffffff;
+      border: 3px solid #808080;
+      border-radius: 3px;
+      height: auto;
+      min-height: 96px;
+      //width: 346px;
+      max-width: 100%;
+
+      > .ias-avatar {
+        flex: 0 0 65px;
+        height: 65px;
+        width: 65px;
+        margin-bottom: 5px;
+      }
 
-  .assistant {
-    display: none;
+      > .ias-tile-content {
+        flex-flow: row nowrap;
+      }
+    }
   }
 
-  // (L) Wide enough to show main person offset to right and display managers horizontally
-  &.large {
-    > .org-chart-section {
-      text-align: left;
-
-      > .ias-tile {
-        &[size="large"] {
-          margin: 0 0 0 128px;
-        }
-      }
+  // (XS) Default display
+  org-chart {
+    display: block;
+    max-width: 100%;
 
-      .org-chart-connector {
-        left: 172px;
-        margin: 0;
-      }
+    .assistant {
+      display: none;
+    }
 
-      &.managers {
-        margin-left: 0;
-        min-height: 128px;
-        overflow: hidden;
-        white-space: nowrap;
+    // (L) Wide enough to show main person offset to right and display managers horizontally
+    &.large {
+      > .org-chart-section {
+        text-align: left;
 
-        > h3 {
-          left: 0;
-          position: absolute;
-          top: 6px;
+        > .ias-tile {
+          &[size="large"] {
+            margin: 0 0 0 128px;
+          }
         }
 
         .org-chart-connector {
-          left: 42px;
+          left: 172px;
+          margin: 0;
         }
 
-        .manager {
-          display: inline-block;
-          text-align: left;
+        &.managers {
           margin-left: 0;
-          margin-bottom: 32px;
+          min-height: 128px;
+          overflow: hidden;
+          white-space: nowrap;
 
-          &:first-child {
-            margin-left: 115px;
+          > h3 {
+            left: 0;
+            position: absolute;
+            top: 6px;
+          }
 
-            > .org-chart-connector {
-              bottom: initial;
-              top: 56px;
-              left: 57px;
-              height: 72px;
-            }
+          .org-chart-connector {
+            left: 42px;
           }
 
-          &:not(:first-child) {
-            > .org-chart-connector {
-              background-color: $org-chart-secondary-connector-color;
-              bottom: initial;
-              height: 3px;
-              left: -37px;
-              top: 26px;
-              width: 69px;
+          .manager {
+            display: inline-block;
+            text-align: left;
+            margin-left: 0;
+            margin-bottom: 32px;
+
+            &:first-child {
+              margin-left: 115px;
+
+              > .org-chart-connector {
+                bottom: initial;
+                top: 56px;
+                left: 57px;
+                height: 72px;
+              }
             }
 
-            .ias-tile {
-              > .ias-avatar {
+            &:not(:first-child) {
+              > .org-chart-connector {
                 background-color: $org-chart-secondary-connector-color;
+                bottom: initial;
+                height: 3px;
+                left: -37px;
+                top: 26px;
+                width: 69px;
+              }
 
-                &:not(:hover) {
-                  border-color: $org-chart-secondary-connector-color;
+              .ias-tile {
+                > .ias-avatar {
+                  background-color: $org-chart-secondary-connector-color;
+
+                  &:not(:hover) {
+                    border-color: $org-chart-secondary-connector-color;
+                  }
                 }
-              }
-              > .ias-tile-content {
-                > .details {
-                  > :first-child {
-                    color: $org-chart-connector-color;
+                > .ias-tile-content {
+                  > .details {
+                    > :first-child {
+                      color: $org-chart-connector-color;
+                    }
                   }
                 }
               }
             }
-          }
 
-          &:not(:last-child) {
-            margin-right: 5px;
+            &:not(:last-child) {
+              margin-right: 5px;
+            }
           }
         }
-      }
 
-      &.self {
-        display: inline-flex;
+        &.self {
+          display: inline-flex;
 
-        > .assistant {
-          display: inline-block;
-          margin-left: 33px;
-          position: relative;
+          > .assistant {
+            display: inline-block;
+            margin-left: 33px;
+            position: relative;
 
-          .ias-tile {
-            > .ias-avatar {
-              background-color: $org-chart-secondary-connector-color;
+            .ias-tile {
+              > .ias-avatar {
+                background-color: $org-chart-secondary-connector-color;
 
-              &:not(:hover) {
-                border-color: $org-chart-secondary-connector-color;
+                &:not(:hover) {
+                  border-color: $org-chart-secondary-connector-color;
+                }
               }
             }
-          }
 
-          > .org-chart-connector {
-            background-color: transparent;
-            border-top: 3px dashed $org-chart-secondary-connector-color;
-            height: 0;
-            left: -37px;
-            top: 26px;
-            width: 69px;
+            > .org-chart-connector {
+              background-color: transparent;
+              border-top: 3px dashed $org-chart-secondary-connector-color;
+              height: 0;
+              left: -37px;
+              top: 26px;
+              width: 69px;
+            }
           }
         }
       }
     }
-  }
 
-  > .org-chart-section {
-    position: relative;
-    text-align: center;
-    width: 100%;
+    > .org-chart-section {
+      position: relative;
+      text-align: center;
+      width: 100%;
 
-    &.direct-reports {
-      > .org-chart-connector {
-        height: 34px;
+      &.direct-reports {
+        > .org-chart-connector {
+          height: 34px;
+        }
       }
-    }
 
-    &.managers {
-      min-height: 98px;
+      &.managers {
+        min-height: 98px;
 
-      &.overflow {
-        .manager {
-          &:last-child {
-            > .ias-tile {
-              > .reports {
-                display: none;
-              }
+        &.overflow {
+          .manager {
+            &:last-child {
+              > .ias-tile {
+                > .reports {
+                  display: none;
+                }
 
-              > .ias-tile-content {
-                > .avatar {
-                  //background-image: url('../../images/icons/m_circle-horz-menu_thin.svg');
+                > .ias-tile-content {
+                  > .avatar {
+                    //background-image: url('../../images/icons/m_circle-horz-menu_thin.svg');
+                  }
                 }
               }
             }
           }
         }
-      }
 
-      .manager {
-        margin-bottom: $manager-connector-height;
-        position: relative;
-        text-align: center;
+        .manager {
+          margin-bottom: $manager-connector-height;
+          position: relative;
+          text-align: center;
 
-        &.empty-manager {
-          > .ias-tile {
-            cursor: initial;
+          &.empty-manager {
+            > .ias-tile {
+              cursor: initial;
 
-            > .ias-tile-content {
-              > .avatar {
-                background: $org-chart-secondary-connector-color;
-                border-color: $org-chart-secondary-connector-color;
+              > .ias-tile-content {
+                > .avatar {
+                  background: $org-chart-secondary-connector-color;
+                  border-color: $org-chart-secondary-connector-color;
+                }
               }
             }
+
+            > .org-chart-connector {
+              background-color: $org-chart-secondary-connector-color;
+            }
           }
 
-          > .org-chart-connector {
-            background-color: $org-chart-secondary-connector-color;
+          > .ias-tile {
+            display: inline-block;
           }
         }
 
-        > .ias-tile {
-          display: inline-block;
+        .org-chart-connector {
+          bottom: -$manager-connector-height;
+          height: $manager-connector-height;
+          top: initial;
         }
       }
 
-      .org-chart-connector {
-        bottom: -$manager-connector-height;
-        height: $manager-connector-height;
-        top: initial;
+      > h3 {
+        color: $org-chart-text-color;
+        font-size: 12px;
+        font-weight: normal;
+        line-height: 14px;
+        margin: 0;
+        padding: 15px 0 5px 0;
+        text-align: left;
       }
-    }
 
-    > h3 {
-      color: $org-chart-text-color;
-      font-size: 12px;
-      font-weight: normal;
-      line-height: 14px;
-      margin: 0;
-      padding: 15px 0 5px 0;
-      text-align: left;
-    }
-
-    > .ias-tile {
-      &[size="large"] {
-        margin: 0 auto;
+      > .ias-tile {
+        &[size="large"] {
+          margin: 0 auto;
+        }
       }
-    }
 
-    > .ias-grid {
-      border-top: 3px solid $org-chart-connector-color;
-      min-height: 90px;
-      padding-top: 5px;
-    }
-
-    .org-chart-connector {
-      background-color: $org-chart-connector-color;
-      left: 0;
-      margin: 0 auto;
-      position: absolute;
-      right: 0;
-      top: 0;
-      width: 5px;
-    }
-  }
-}
+      > .ias-grid {
+        border-top: 3px solid $org-chart-connector-color;
+        min-height: 90px;
+        padding-top: 5px;
+      }
 
-[dir="rtl"] {
-  .assistant,
-  .manager {
-    .reports {
-      left: 20px;
-      right: auto;
+      .org-chart-connector {
+        background-color: $org-chart-connector-color;
+        left: 0;
+        margin: 0 auto;
+        position: absolute;
+        right: 0;
+        top: 0;
+        width: 5px;
+      }
     }
   }
 
-  // (XS) Default display
-  org-chart {
-    > .org-chart-section {
-      > h3 {
-        text-align: right;
+  [dir="rtl"] {
+    .assistant,
+    .manager {
+      .reports {
+        left: 20px;
+        right: auto;
       }
     }
 
-    // (L) Wide enough to show main person offset to right and display managers horizontally
-    &.large {
+    // (XS) Default display
+    org-chart {
       > .org-chart-section {
-        text-align: right;
-
-        > .ias-tile {
-          &[size="large"] {
-            margin: 0 128px 0 0;
-          }
-
-          .reports {
-            left: 3px;
-            right: initial;
-          }
+        > h3 {
+          text-align: right;
         }
+      }
 
-        .org-chart-connector {
-          left: initial;
-          right: 172px;
-        }
+      // (L) Wide enough to show main person offset to right and display managers horizontally
+      &.large {
+        > .org-chart-section {
+          text-align: right;
 
-        &.managers {
-          margin-left: auto;
+          > .ias-tile {
+            &[size="large"] {
+              margin: 0 128px 0 0;
+            }
 
-          > h3 {
-            left: initial;
-            right: 0;
+            .reports {
+              left: 3px;
+              right: initial;
+            }
           }
 
           .org-chart-connector {
             left: initial;
-            right: 42px;
+            right: 172px;
           }
 
-          .manager {
-            margin-left: 5px;
-            margin-right: 0;
-            text-align: right;
+          &.managers {
+            margin-left: auto;
 
-            &:first-child {
-              margin-right: 115px;
+            > h3 {
+              left: initial;
+              right: 0;
+            }
 
-              > .org-chart-connector {
-                left: initial;
-                right: 57px;
-              }
+            .org-chart-connector {
+              left: initial;
+              right: 42px;
             }
 
-            &:not(:first-child) {
-              > .org-chart-connector {
-                left: initial;
-                right: -37px;
+            .manager {
+              margin-left: 5px;
+              margin-right: 0;
+              text-align: right;
+
+              &:first-child {
+                margin-right: 115px;
+
+                > .org-chart-connector {
+                  left: initial;
+                  right: 57px;
+                }
               }
-            }
 
-            &:last-child {
-              margin-left: 0;
+              &:not(:first-child) {
+                > .org-chart-connector {
+                  left: initial;
+                  right: -37px;
+                }
+              }
+
+              &:last-child {
+                margin-left: 0;
+              }
             }
           }
-        }
 
-        &.self {
-          > .assistant {
-            margin-left: 0;
-            margin-right: 33px;
+          &.self {
+            > .assistant {
+              margin-left: 0;
+              margin-right: 33px;
 
-            > .org-chart-connector {
-              left: auto;
-              right: -37px;
+              > .org-chart-connector {
+                left: auto;
+                right: -37px;
+              }
             }
           }
         }
       }
     }
   }
-}
+}

+ 48 - 46
client/src/modules/peoplesearch/peoplesearch-cards.component.scss

@@ -20,71 +20,73 @@
  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
  */
 
-people-search-cards {
-  #page-content-title {
-    margin-bottom: 0;
-  }
+.ias-styles-root {
+  people-search-cards {
+    #page-content-title {
+      margin-bottom: 0;
+    }
 
-  display: flex;
-  flex-flow: column nowrap;
-  height: 100%;
+    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 {
-          margin: 0 auto;
-          display: block;
-          width: 272px;
+    // At medium size, cards are centered and no longer take up 100% width
+    &.medium {
+      > .people-search-component-content {
+        > .person-card-list {
+          > person-card {
+            margin: 0 auto;
+            display: block;
+            width: 272px;
+          }
         }
       }
     }
-  }
 
-  // At large size, cards fit next to each other
-  &.large {
-    > .people-search-component-content {
-      > .person-card-list {
-        text-align: left;
-        margin: 0;
+    // 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;
+          > person-card {
+            display: inline-block;
+            margin-right: 5px;
+          }
         }
       }
     }
-  }
 
-  > .people-search-component-content {
-    flex: 1 1;
-    overflow: auto;
-    text-align: center;
+    > .people-search-component-content {
+      flex: 1 1;
+      overflow: auto;
+      text-align: center;
 
-    > .person-card-list {
-      > person-card {
-        display: inline-block;
-        width: 100%;
+      > .person-card-list {
+        > person-card {
+          display: inline-block;
+          width: 100%;
 
-        &:not(:last-child) {
-          margin-bottom: 5px;
+          &:not(:last-child) {
+            margin-bottom: 5px;
+          }
         }
       }
     }
   }
-}
 
-[dir="rtl"] {
-  people-search-cards {
-    &.large {
-      > .people-search-component-content {
-        .person-card-list {
-          text-align: right;
+  [dir="rtl"] {
+    people-search-cards {
+      &.large {
+        > .people-search-component-content {
+          .person-card-list {
+            text-align: right;
 
-          > person-card {
-            margin-right: auto;
-            margin-left: 5px;
+            > person-card {
+              margin-right: auto;
+              margin-left: 5px;
+            }
           }
         }
       }

+ 18 - 17
client/src/modules/peoplesearch/peoplesearch-table.component.scss

@@ -20,26 +20,27 @@
  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
  */
 
+.ias-styles-root {
+    people-search-table {
+        #page-content-title {
+            margin-bottom: 0;
+        }
 
-people-search-table {
-    #page-content-title {
-        margin-bottom: 0;
-    }
-
-    display: flex;
-    flex-flow: column nowrap;
-    height: 100%;
+        display: flex;
+        flex-flow: column nowrap;
+        height: 100%;
 
-    > .people-search-component-content {
-        flex: 1 1;
-        overflow: auto;
-        position: relative;
-        text-align: center;
+        > .people-search-component-content {
+            flex: 1 1;
+            overflow: auto;
+            position: relative;
+            text-align: center;
 
-        .table-configuration-menu-toggle {
-            position: absolute;
-            top: 0;
-            right: 0;
+            .table-configuration-menu-toggle {
+                position: absolute;
+                top: 0;
+                right: 0;
+            }
         }
     }
 }

+ 8 - 2
client/src/modules/peoplesearch/peoplesearch.module.ts

@@ -20,6 +20,10 @@
  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
  */
 
+// These need to be at the top so imported components can override the default styling
+require('../../styles.scss');
+require('./peoplesearch.scss');
+
 import 'angular-aria';
 
 import {IComponentOptions, module} from 'angular';
@@ -35,8 +39,8 @@ import LocalStorageService from '../../services/local-storage.service';
 import PromiseService from '../../services/promise.service';
 import uxModule from '../../ux/ux.module';
 import CommonSearchService from '../../services/common-search.service';
-
-require('./peoplesearch.scss');
+import OrgchartExportController from './orgchart-export.controller';
+import OrgchartEmailController from './orgchart-email.controller';
 
 const moduleName = 'people-search';
 
@@ -53,6 +57,8 @@ module(moduleName, [
     .component('peopleSearchTable', PeopleSearchTableComponent as IComponentOptions)
     .component('peopleSearchCards', PeopleSearchCardsComponent as IComponentOptions)
     .component('personDetailsDialogComponent', PersonDetailsDialogComponent as IComponentOptions)
+    .controller('OrgchartExportController', OrgchartExportController)
+    .controller('OrgchartEmailController', OrgchartEmailController)
     .service('PromiseService', PromiseService)
     .service('LocalStorageService', LocalStorageService)
     .service('CommonSearchService', CommonSearchService);

+ 90 - 106
client/src/modules/peoplesearch/peoplesearch.scss

@@ -21,141 +21,125 @@
  */
 
 
-body, html {
-  height: 100%;
-}
-
-body {
-  background-color: transparent;  // Can remove once https://github.com/MicroFocus/ux-ias/issues/22 is resolved
-
-  > ui-view {
-    box-sizing: border-box;
-    display: block;
-    height: 100%;
-    overflow: hidden;
-    padding: 5px;
-    width: 100%;
-  }
-}
-
-.help-desk-search-component,
-.people-search-component {
-  height: 100%;
-
-  > ui-view {
+.ias-styles-root {
+  .help-desk-search-component,
+  .people-search-component {
     height: 100%;
-    overflow: auto;
-  }
 
-  .peoplesearch-header {
-    display: flex;
-    align-items: flex-start;
-
-    .basic-search-container {
-      display: flex;
-      align-items: center;
-      margin-bottom: 15px;
-
-      > * + * {
-        margin-left: 10px;
-      }
+    > ui-view {
+      height: 100%;
+      overflow: auto;
     }
 
-    .advanced-search-container {
+    .peoplesearch-header {
       display: flex;
-      flex-direction: column;
       align-items: flex-start;
-      margin-bottom: 15px;
 
-      > * + * {
-        margin-top: 5px;
-      }
+      .basic-search-container {
+        display: flex;
+        align-items: center;
+        margin-bottom: 15px;
 
-      &+ div {
-        margin-top: 15px;
+        > * + * {
+          margin-left: 10px;
+        }
       }
 
-      select.attribute-value {
-        min-width: 210px;
+      .advanced-search-container {
+        display: flex;
+        flex-direction: column;
+        align-items: flex-start;
+        margin-bottom: 15px;
+
+        > * + * {
+          margin-top: 5px;
+        }
+
+        & + div {
+          margin-top: 15px;
+        }
+
+        select.attribute-value {
+          min-width: 210px;
+        }
       }
     }
-  }
 
-  .search-info-container {
-    text-align: left;
-
-    .search-info {
-      background-color: #fff6ce;
-      border: 1px solid #dae1e1;
-      border-radius: 3px;
-      color: #808080;
-      display: inline-block;
-      font-size: 14px;
-      margin: 0 auto 10px;
-      padding: 5px;
-
-      &.loading {
-        background-color: white;
+    .search-info-container {
+      text-align: left;
+
+      .search-info {
+        background-color: #fff6ce;
+        border: 1px solid #dae1e1;
+        border-radius: 3px;
+        color: #808080;
+        display: inline-block;
+        font-size: 14px;
+        margin: 0 auto 10px;
+        padding: 5px;
+
+        &.loading {
+          background-color: white;
+        }
       }
     }
   }
-}
 
-.highlight {
-  color: #01a9e7;
-}
-
-.ias-avatar {
-  background: transparent url('../../../images/user.png') no-repeat center center;
-  background-size: contain;
-}
-
-.ias-header {
-  max-width: 100%;
-}
+  .highlight {
+    color: #01a9e7;
+  }
 
-.checkbox-button {
-  align-items: center;
-  display: flex;
-  flex-flow: row nowrap;
-}
+  .ias-avatar {
+    background: transparent url('../../../images/user.png') no-repeat center center;
+    background-size: contain;
+  }
 
-.icon-divider {
-  &.vertical {
-    background-color: rgba(#808080, .5);
-    height: 25px;
-    margin: 0 5px;
-    width: 1px;
+  .ias-header {
+    max-width: 100%;
   }
-}
 
-.single-line {
-  overflow: hidden;
-  text-overflow: ellipsis;
-  white-space: nowrap;
-}
+  .checkbox-button {
+    align-items: center;
+    display: flex;
+    flex-flow: row nowrap;
+  }
 
-@media (min-width: 768px) {
-  .ias-dialog {
-    .ias-dialog-container {
-      max-width: 100%;
-      width: 375px;
+  .icon-divider {
+    &.vertical {
+      background-color: rgba(#808080, .5);
+      height: 25px;
+      margin: 0 5px;
+      width: 1px;
     }
   }
-}
 
+  .single-line {
+    overflow: hidden;
+    text-overflow: ellipsis;
+    white-space: nowrap;
+  }
 
-[dir="rtl"] {
-  .ias-search {
-    >.ias-icon-button {
-      right: auto;         // Can remove once https://github.com/MicroFocus/ux-ias/issues/18 is fixed
+  @media (min-width: 768px) {
+    .ias-dialog {
+      .ias-dialog-container {
+        max-width: 100%;
+        width: 375px;
+      }
     }
   }
 
-  .help-desk-search-component,
-  .people-search-component {
-    .search-info-container {
-      text-align: right;
+  [dir="rtl"] {
+    .ias-search {
+      > .ias-icon-button {
+        right: auto; // Can remove once https://github.com/MicroFocus/ux-ias/issues/18 is fixed
+      }
+    }
+
+    .help-desk-search-component,
+    .people-search-component {
+      .search-info-container {
+        text-align: right;
+      }
     }
   }
 }

+ 79 - 67
client/src/modules/peoplesearch/person-details-dialog.component.html

@@ -20,80 +20,92 @@
   ~ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
   -->
 
-<div class="ias-dialog person-details-dialog">
-    <div class="ias-dialog-container">
-        <div class="ias-dialog-content">
-            <div class="person-details-dialog-header">
-                <div class="ias-avatar" ng-if="$ctrl.photosEnabled" ng-style="$ctrl.getAvatarStyle()" alt="User image"></div>
+<div class="ias-styles-root">
+    <div class="ias-dialog person-details-dialog">
+        <div class="ias-dialog-container">
+            <div class="ias-dialog-content">
+                <div class="person-details-dialog-header">
+                    <div class="ias-avatar" ng-if="$ctrl.photosEnabled" ng-style="$ctrl.getAvatarStyle()" alt="User image"></div>
 
-                <div class="ias-header">
-                    <h2 ng-bind="$ctrl.person.displayNames[0]"></h2>
-                    <span class="ias-fill"></span>
-                    <ias-button class="ias-icon-button" ng-click="$ctrl.closeDialog()">
-                        <ias-icon icon="close_thick"></ias-icon>
-                    </ias-button>
+                    <div class="ias-header">
+                        <h2 ng-bind="$ctrl.person.displayNames[0]"></h2>
+                        <span class="ias-fill"></span>
+                        <ias-button class="ias-icon-button" ng-click="$ctrl.closeDialog()">
+                            <ias-icon icon="close_thick"></ias-icon>
+                        </ias-button>
+                    </div>
+                    <div ng-bind="$ctrl.person.displayNames[1]"></div>
+                    <div ng-bind="$ctrl.person.displayNames[2]"></div>
+                    <div ng-bind="$ctrl.person.displayNames[3]"></div>
+                    <div class="person-dialog-actions">
+                        <ias-button type="button" class="ias-icon-text-button"
+                                    ng-click="$ctrl.gotoOrgChart()" ng-if="$ctrl.orgChartEnabled">
+                            <ias-icon icon="orgchart_thin" id="orgchart-button"></ias-icon>
+                            <span translate="Title_OrgChart">Organizational Chart</span>
+                        </ias-button>
+                        <ias-button class="ias-icon-button ias-dialog-cancel-button"
+                                    ng-attr-title="{{ 'Button_ExportOrgChart' | translate }}"
+                                    ng-click="$ctrl.beginExport()" ng-if="$ctrl.exportEnabled">
+                            <ias-icon icon="download_thick"></ias-icon>
+                        </ias-button>
+                        <ias-button class="ias-icon-button ias-dialog-cancel-button"
+                                    ng-attr-title="{{ 'Button_EmailTeam' | translate }}"
+                                    ng-click="$ctrl.beginEmail()" ng-if="$ctrl.emailTeamEnabled">
+                            <ias-icon icon="email_thick"></ias-icon>
+                        </ias-button>
+                    </div>
                 </div>
-                <div ng-bind="$ctrl.person.displayNames[1]"></div>
-                <div ng-bind="$ctrl.person.displayNames[2]"></div>
-                <div ng-bind="$ctrl.person.displayNames[3]"></div>
-                <div class="person-dialog-actions">
-                    <ias-button type="button" class="ias-icon-text-button"
-                                ng-click="$ctrl.gotoOrgChart()" ng-if="$ctrl.orgChartEnabled">
-                        <ias-icon icon="orgchart_thin" id="orgchart-button"></ias-icon>
-                        <span translate="Title_OrgChart">Organizational Chart</span>
-                    </ias-button>
-                </div>
-            </div>
-            <div class="person-details-content">
-                <table class="details-table">
-                    <tbody>
-                    <tr>
-                        <td></td>
-                        <td>
-                            <ul>
-                                <li ng-repeat="reference in $ctrl.person.links">
-                                    <a ng-href="{{reference.link}}"><span ng-bind="reference.name"></span></a>
-                                </li>
-                            </ul>
-                        </td>
-                    </tr>
-                    <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">
+                <div class="person-details-content">
+                    <table class="details-table">
+                        <tbody>
+                        <tr>
+                            <td></td>
+                            <td>
                                 <ul>
-                                    <li ng-repeat="user in detail.userReferences">
-                                        <a ng-href="{{$ctrl.getPersonDetailsUrl(user.userKey)}}"
-                                           ng-bind="user.displayName"></a>
+                                    <li ng-repeat="reference in $ctrl.person.links">
+                                        <a ng-href="{{reference.link}}"><span ng-bind="reference.name"></span></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'"></a>
-                                        <a ng-href="tel:{{value}}"
-                                           ng-bind="value"
-                                           ng-if="detail.type === 'tel'"></a>
-                                        <span ng-bind="value"
-                                              ng-if="detail.type !== 'email' && detail.type !== 'tel'"></span>
+                            </td>
+                        </tr>
+                        <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'"></a>
+                                            <a ng-href="tel:{{value}}"
+                                               ng-bind="value"
+                                               ng-if="detail.type === 'tel'"></a>
+                                            <span ng-bind="value"
+                                                  ng-if="detail.type !== 'email' && detail.type !== 'tel'"></span>
 
-                                        <a ui-sref="search.table({ query: value })"
-                                           class="details-table-search-link"
-                                           ng-if="detail.searchable"
-                                           ng-attr-title="{{('Placeholder_Search' | translate) + ' \'' + value + '\''}}">
-                                            <ias-icon icon="search_thick"></ias-icon>
-                                        </a>
+                                            <a ui-sref="search.table({ query: value })"
+                                               class="details-table-search-link"
+                                               ng-if="detail.searchable"
+                                               ng-attr-title="{{('Placeholder_Search' | translate) + ' \'' + value + '\''}}">
+                                                <ias-icon icon="search_thick"></ias-icon>
+                                            </a>
 
-                                    </li>
-                                </ul>
-                            </div>
-                        </td>
-                    </tr>
-                    </tbody>
-                </table>
+                                        </li>
+                                    </ul>
+                                </div>
+                            </td>
+                        </tr>
+                        </tbody>
+                    </table>
+                </div>
             </div>
         </div>
     </div>

+ 77 - 67
client/src/modules/peoplesearch/person-details-dialog.component.scss

@@ -20,90 +20,100 @@
  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
  */
 
-.person-dialog-actions {
-  clear: both;
-  margin-top: 15px;
-  text-align: center;
-}
-
-.person-details-dialog {
-  text-align: left;
-  overflow: hidden;
+.ias-styles-root {
+  .person-dialog-actions {
+    clear: both;
+    margin-top: 15px;
+    text-align: center;
+
+    > .ias-icon-text-button + .ias-icon-button {
+      margin-left: 20px;
+    }
 
-  .ias-dialog-container {
-    padding: 0;
+    > .ias-icon-button + .ias-icon-button {
+      margin-left: 10px;
+    }
   }
 
-  .ias-avatar {
-    float: left;
-    height: 100px;
-    margin-bottom: 15px;
-    margin-right: 15px;
-    width: 100px;
-  }
-}
+  .person-details-dialog {
+    text-align: left;
+    overflow: hidden;
 
-.person-details-dialog-header {
-  background-color: #eef2f2;
-  color: #434c50;
-  font-size: 12px;
-  line-height: 15px;
-  padding: 15px;
-}
+    .ias-dialog-container {
+      padding: 0;
+    }
 
-[data-browserType=ie] .person-details-dialog-header > .ias-header > h2 {
-  width: 185px;
-}
+    .ias-avatar {
+      float: left;
+      height: 100px;
+      margin-bottom: 15px;
+      margin-right: 15px;
+      width: 100px;
+    }
+  }
 
-.person-details-content {
-  padding: 15px;
-}
+  .person-details-dialog-header {
+    background-color: #eef2f2;
+    color: #434c50;
+    font-size: 12px;
+    line-height: 15px;
+    padding: 15px;
+  }
 
-.details-table {
-  border: none;
-  border-collapse: collapse;
-  width: 100%;
-
-  tr {
-    height: 25px;
-
-    td {
-      border: none;
-      font-size: 12px;
-      height: 19px;
-      text-align: left;
-
-      &:first-child {
-        color: #949494;
-        width: 100px;
-        text-align: right;
-        padding: 3px 0;
-      }
+  [data-browserType=ie] .person-details-dialog-header > .ias-header > h2 {
+    width: 185px;
+  }
 
-      &:last-child {
-        padding: 3px 15px;
-      }
+  .person-details-content {
+    padding: 15px;
+  }
 
-      ul {
-        list-style: none;
-        margin: 0;
-        padding: 0;
+  .details-table {
+    border: none;
+    border-collapse: collapse;
+    width: 100%;
+
+    tr {
+      height: 25px;
+
+      td {
+        border: none;
+        font-size: 12px;
+        height: 19px;
+        text-align: left;
+
+        &:first-child {
+          color: #949494;
+          width: 100px;
+          text-align: right;
+          padding: 3px 0;
+        }
 
-        > li {
+        &:last-child {
+          padding: 3px 15px;
+        }
+
+        ul {
+          list-style: none;
           margin: 0;
           padding: 0;
+
+          > li {
+            margin: 0;
+            padding: 0;
+          }
         }
       }
     }
-  }
 
-  .details-table-search-link {
-    display: inline-block;
-    font-size: 25px;
-    vertical-align: middle;
+    .details-table-search-link {
+      display: inline-block;
+      font-size: 25px;
+      vertical-align: middle;
 
-    .ias-icon {
-      display:block;
+      .ias-icon {
+        display: block;
+      }
     }
   }
 }

+ 59 - 5
client/src/modules/peoplesearch/person-details-dialog.component.ts

@@ -22,10 +22,14 @@
 
 
 import { Component } from '../../component';
-import { IPeopleSearchConfigService } from '../../services/peoplesearch-config.service';
+import {IPeopleSearchConfigService, IPersonDetailsConfig} from '../../services/peoplesearch-config.service';
 import { IPeopleService } from '../../services/people.service';
-import { IAugmentedJQuery, ITimeoutService } from 'angular';
+import {IAugmentedJQuery, ITimeoutService, noop} from 'angular';
 import { IPerson } from '../../models/person.model';
+import {IChangePasswordSuccess} from '../../components/changepassword/success-change-password.controller';
+
+let orgchartExportTemplateUrl = require('./orgchart-export.component.html');
+let orgchartEmailTemplateUrl = require('./orgchart-email.component.html');
 
 @Component({
     stylesheetUrl: require('./person-details-dialog.component.scss'),
@@ -35,15 +39,28 @@ export default class PersonDetailsDialogComponent {
     person: IPerson;
     photosEnabled: boolean;
     orgChartEnabled: boolean;
-
-    static $inject = [ '$element', '$state', '$stateParams', '$timeout', 'ConfigService', 'PeopleService' ];
+    exportEnabled: boolean;
+    emailTeamEnabled: boolean;
+    maxExportDepth: number;
+    maxEmailDepth: number;
+
+    static $inject = [
+        '$element',
+        '$state',
+        '$stateParams',
+        '$timeout',
+        'ConfigService',
+        'PeopleService',
+        'IasDialogService'
+    ];
 
     constructor(private $element: IAugmentedJQuery,
                 private $state: angular.ui.IStateService,
                 private $stateParams: angular.ui.IStateParamsService,
                 private $timeout: ITimeoutService,
                 private configService: IPeopleSearchConfigService,
-                private peopleService: IPeopleService) {
+                private peopleService: IPeopleService,
+                private IasDialogService: any) {
     }
 
     $onInit(): void {
@@ -57,6 +74,15 @@ export default class PersonDetailsDialogComponent {
             this.photosEnabled = photosEnabled;
         });
 
+        this.configService.personDetailsConfig().then((personDetailsConfig: IPersonDetailsConfig) => {
+            this.photosEnabled = personDetailsConfig.photosEnabled;
+            this.orgChartEnabled = personDetailsConfig.orgChartEnabled;
+            this.exportEnabled = personDetailsConfig.exportEnabled;
+            this.emailTeamEnabled = personDetailsConfig.emailTeamEnabled;
+            this.maxExportDepth = personDetailsConfig.maxExportDepth;
+            this.maxEmailDepth = personDetailsConfig.maxEmailDepth;
+        });
+
         this.peopleService
             .getPerson(personId)
             .then(
@@ -98,4 +124,32 @@ export default class PersonDetailsDialogComponent {
     searchText(text: string): void {
         this.$state.go('search.table', { query: text });
     }
+
+    beginExport() {
+        this.IasDialogService
+            .open({
+                controller: 'OrgchartExportController as $ctrl',
+                templateUrl: orgchartExportTemplateUrl,
+                locals: {
+                    peopleService: this.peopleService,
+                    maxDepth: this.maxExportDepth,
+                    personName: this.person.displayNames[0],
+                    userKey: this.person.userKey
+                }
+            });
+    }
+
+    beginEmail() {
+        this.IasDialogService
+            .open({
+                controller: 'OrgchartEmailController as $ctrl',
+                templateUrl: orgchartEmailTemplateUrl,
+                locals: {
+                    peopleService: this.peopleService,
+                    maxDepth: this.maxEmailDepth,
+                    personName: this.person.displayNames[0],
+                    userKey: this.person.userKey
+                }
+            });
+    }
 }

+ 8 - 1
client/src/services/base-config.service.ts

@@ -24,7 +24,8 @@ import {IHttpService, ILogService, IPromise, IQService} from 'angular';
 import {IPwmService} from './pwm.service';
 
 const COLUMN_CONFIG = 'searchColumns';
-const PHOTO_ENABLED = 'enablePhoto';
+export const PHOTO_ENABLED = 'enablePhoto';
+const PRINTING_ENABLED = 'enableOrgChartPrinting';
 
 export const ADVANCED_SEARCH_ENABLED = 'enableAdvancedSearch';
 export const ADVANCED_SEARCH_MAX_ATTRIBUTES = 'maxAdvancedSearchAttributes';
@@ -34,6 +35,7 @@ export interface IConfigService {
     getColumnConfig(): IPromise<any>;
     getValue(key: string): IPromise<any>;
     photosEnabled(): IPromise<boolean>;
+    printingEnabled(): IPromise<boolean>;
 }
 
 export interface IAttributeMetadata {
@@ -98,4 +100,9 @@ export abstract class ConfigBaseService implements IConfigService {
         return this.getValue(PHOTO_ENABLED)
             .then(null, () => { return true; }); // On error use default
     }
+
+    printingEnabled(): IPromise<boolean> {
+        return this.getValue(PRINTING_ENABLED)
+            .then(null, () => { return true; }); // On error use default
+    }
 }

+ 21 - 1
client/src/services/people.service.ts

@@ -21,7 +21,7 @@
  */
 
 
-import {IHttpService, ILogService, IPromise, IQService, IWindowService} from 'angular';
+import {IDeferred, IHttpService, ILogService, IPromise, IQService, IWindowService} from 'angular';
 import {IPerson} from '../models/person.model';
 import IPwmService from './pwm.service';
 import IOrgChartData from '../models/orgchart-data.model';
@@ -48,6 +48,8 @@ export interface IPeopleService {
 
     getPerson(id: string): IPromise<IPerson>;
 
+    getTeamEmails(id: string, depth: number): IPromise<string[]>;
+
     search(query: string): IPromise<SearchResult>;
 }
 
@@ -223,6 +225,24 @@ export default class PeopleService implements IPeopleService {
         return promise;
     }
 
+    getTeamEmails(id: string, depth: number): IPromise<string[]> {
+        const deferredValue: IDeferred<string[]> = this.$q.defer();
+
+        let request = this.$http
+            .get(this.pwmService.getServerUrl('mailtoLinks'), {
+                cache: true,
+                params: {
+                    userKey: id,
+                    depth: depth
+                }
+            })
+            .then((response) => {
+                deferredValue.resolve(response.data['data']);
+            });
+
+        return deferredValue.promise;
+    }
+
     search(query: string, params?: any): IPromise<SearchResult> {
         // Deferred object used for aborting requests. See promise.service.ts for more information
         let httpTimeout = this.$q.defer();

+ 36 - 2
client/src/services/peoplesearch-config.service.ts

@@ -21,7 +21,7 @@
  */
 
 
-import { IHttpService, ILogService, IPromise, IQService } from 'angular';
+import {IDeferred, IHttpService, ILogService, IPromise, IQService} from 'angular';
 import IPwmService from './pwm.service';
 import PwmService from './pwm.service';
 import {
@@ -30,18 +30,32 @@ import {
     IAdvancedSearchConfig,
     ADVANCED_SEARCH_ENABLED,
     ADVANCED_SEARCH_MAX_ATTRIBUTES,
-    ADVANCED_SEARCH_ATTRIBUTES
+    ADVANCED_SEARCH_ATTRIBUTES, PHOTO_ENABLED
 } from './base-config.service';
 
 const ORGCHART_ENABLED = 'orgChartEnabled';
 const ORGCHART_MAX_PARENTS = 'orgChartMaxParents';
 const ORGCHART_SHOW_CHILD_COUNT = 'orgChartShowChildCount';
+const EXPORT_ENABLED = 'enableExport';
+const EXPORT_MAX_DEPTH = 'exportMaxDepth';
+const MAILTO_ENABLED = 'enableMailtoLinks';
+const MAILTO_MAX_DEPTH = 'mailtoLinkMaxDepth';
 
 export interface IPeopleSearchConfigService extends IConfigService {
     getOrgChartMaxParents(): IPromise<number>;
     orgChartEnabled(): IPromise<boolean>;
     orgChartShowChildCount(): IPromise<boolean>;
     advancedSearchConfig(): IPromise<IAdvancedSearchConfig>;
+    personDetailsConfig(): IPromise<IPersonDetailsConfig>;
+}
+
+export interface IPersonDetailsConfig {
+    photosEnabled: boolean;
+    orgChartEnabled: boolean;
+    exportEnabled: boolean;
+    emailTeamEnabled: boolean;
+    maxExportDepth: number;
+    maxEmailDepth: number;
 }
 
 export default class PeopleSearchConfigService
@@ -66,6 +80,26 @@ export default class PeopleSearchConfigService
         return this.getValue(ORGCHART_SHOW_CHILD_COUNT);
     }
 
+    personDetailsConfig(): IPromise<IPersonDetailsConfig> {
+        return this.$q.all([
+            this.getValue(PHOTO_ENABLED),
+            this.getValue(ORGCHART_ENABLED),
+            this.getValue(EXPORT_ENABLED),
+            this.getValue(EXPORT_MAX_DEPTH),
+            this.getValue(MAILTO_ENABLED),
+            this.getValue(MAILTO_MAX_DEPTH),
+        ]).then((results: any[]) => {
+            return {
+                photosEnabled: results[0],
+                orgChartEnabled: results[1],
+                exportEnabled: results[2],
+                maxExportDepth: results[3],
+                emailTeamEnabled: results[4],
+                maxEmailDepth: results[5]
+            }
+        });
+    }
+
     advancedSearchConfig(): IPromise<IAdvancedSearchConfig> {
         return this.$q.all([
             this.getValue(ADVANCED_SEARCH_ENABLED),

+ 14 - 0
client/src/styles.scss

@@ -0,0 +1,14 @@
+// Only apply the ux-ias styles when the elements are a child of .ias-styles-root.  This ensures we are not mucking with the
+// styles for the header and footer (things outside of this angular app).
+.ias-styles-root {
+  @import "~@microfocus/ux-ias/src/ux-ias";
+
+  // ux-ias normally applies the following to the body tag, but the way we are including ux-ias under .ias-styles-root
+  // means we have to define it here.
+  background-color: $body-bg-color;
+  color: $text-color;
+  font-family: $font-family;
+  font-size: $font-size;
+  font-weight: $font-weight;
+  line-height: normal;
+}

+ 0 - 5
data-service/pom.xml

@@ -172,10 +172,5 @@
             <artifactId>xodus-environment</artifactId>
             <version>1.2.3</version>
         </dependency>
-        <dependency>
-            <groupId>org.webjars</groupId>
-            <artifactId>webjars-locator-core</artifactId>
-            <version>0.35</version>
-        </dependency>
     </dependencies>
 </project>

+ 1 - 1
docker/pom.xml

@@ -25,7 +25,7 @@
             <plugin>
                 <groupId>com.google.cloud.tools</groupId>
                 <artifactId>jib-maven-plugin</artifactId>
-                <version>0.9.13</version>
+                <version>0.10.0</version>
                 <executions>
                     <execution>
                         <id>make-docker-image</id>

+ 1 - 1
onejar/pom.xml

@@ -17,7 +17,7 @@
 
     <properties>
         <project.root.basedir>${project.basedir}/..</project.root.basedir>
-        <tomcat.version>9.0.12</tomcat.version>
+        <tomcat.version>9.0.13</tomcat.version>
         <jetty-version>9.4.11.v20180605</jetty-version>
     </properties>
 

+ 4 - 4
pom.xml

@@ -235,12 +235,12 @@
             <plugin>
                 <groupId>com.github.spotbugs</groupId>
                 <artifactId>spotbugs-maven-plugin</artifactId>
-                <version>3.1.7</version>
+                <version>3.1.8</version>
                 <dependencies>
                     <dependency>
                         <groupId>com.github.spotbugs</groupId>
                         <artifactId>spotbugs</artifactId>
-                        <version>3.1.8</version>
+                        <version>3.1.9</version>
                     </dependency>
                 </dependencies>
                 <configuration>
@@ -266,7 +266,7 @@
             <plugin> <!-- checks owsp vulnerability database -->
                 <groupId>org.owasp</groupId>
                 <artifactId>dependency-check-maven</artifactId>
-                <version>3.3.2</version>
+                <version>4.0.0</version>
                 <reportSets>
                     <reportSet>
                         <reports>
@@ -289,7 +289,7 @@
         <dependency>
             <groupId>com.github.spotbugs</groupId>
             <artifactId>spotbugs-annotations</artifactId>
-            <version>3.1.8</version>
+            <version>3.1.9</version>
             <scope>provided</scope>
         </dependency>
     </dependencies>

+ 0 - 6
rest-test-service/pom.xml

@@ -66,12 +66,6 @@
             <version>4.12</version>
             <scope>test</scope>
         </dependency>
-        <dependency>
-            <groupId>org.mockito</groupId>
-            <artifactId>mockito-core</artifactId>
-            <version>2.23.0</version>
-            <scope>test</scope>
-        </dependency>
         <dependency>
             <groupId>org.assertj</groupId>
             <artifactId>assertj-core</artifactId>

+ 3 - 3
server/pom.xml

@@ -107,7 +107,7 @@
         <dependency>
             <groupId>org.mockito</groupId>
             <artifactId>mockito-core</artifactId>
-            <version>2.23.0</version>
+            <version>2.23.4</version>
             <scope>test</scope>
         </dependency>
         <dependency>
@@ -225,7 +225,7 @@
         <dependency>
             <groupId>org.jasig.cas.client</groupId>
             <artifactId>cas-client-core</artifactId>
-            <version>3.5.0</version>
+            <version>3.5.1</version>
         </dependency>
         <dependency>
             <groupId>net.glxn</groupId>
@@ -270,7 +270,7 @@
         <dependency>
             <groupId>com.blueconic</groupId>
             <artifactId>browscap-java</artifactId>
-            <version>1.2.5</version>
+            <version>1.2.6</version>
         </dependency>
         <dependency>
             <groupId>org.jetbrains.xodus</groupId>

+ 1 - 0
server/src/main/java/password/pwm/AppProperty.java

@@ -282,6 +282,7 @@ public enum AppProperty
     PEOPLESEARCH_MAX_VALUE_VERIFYUSERDN             ( "peoplesearch.values.verifyUserDN" ),
     PEOPLESEARCH_VALUE_MAXCOUNT                     ( "peoplesearch.values.maxCount" ),
     PEOPLESEARCH_VIEW_DETAIL_LINKS                  ( "peoplesearch.view.detail.links" ),
+    PEOPLESEARCH_MAILTO_MAX_DEPTH                   ( "peoplesearch.mailto.maxDepth" ),
     QUEUE_EMAIL_RETRY_TIMEOUT_MS                    ( "queue.email.retryTimeoutMs" ),
     QUEUE_EMAIL_MAX_COUNT                           ( "queue.email.maxCount" ),
     QUEUE_EMAIL_MAX_THREADS                         ( "queue.email.maxThreads" ),

+ 6 - 0
server/src/main/java/password/pwm/PwmApplication.java

@@ -35,6 +35,7 @@ import password.pwm.error.PwmError;
 import password.pwm.error.PwmException;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.health.HealthMonitor;
+import password.pwm.http.servlet.peoplesearch.PeopleSearchService;
 import password.pwm.http.servlet.resource.ResourceServletService;
 import password.pwm.http.state.SessionStateService;
 import password.pwm.ldap.LdapConnectionService;
@@ -631,6 +632,11 @@ public class PwmApplication
         return ( ResourceServletService ) pwmServiceManager.getService( ResourceServletService.class );
     }
 
+    public PeopleSearchService getPeopleSearchService( )
+    {
+        return ( PeopleSearchService ) pwmServiceManager.getService( PeopleSearchService.class );
+    }
+
     public Configuration getConfig( )
     {
         return pwmEnvironment.getConfig();

+ 4 - 0
server/src/main/java/password/pwm/config/PwmSetting.java

@@ -976,6 +976,10 @@ public enum PwmSetting
             "peopleSearch.enableOrgChart", PwmSettingSyntax.BOOLEAN, PwmSettingCategory.PEOPLE_SEARCH ),
     PEOPLE_SEARCH_ENABLE_EXPORT(
             "peopleSearch.enableExport", PwmSettingSyntax.BOOLEAN, PwmSettingCategory.PEOPLE_SEARCH ),
+    PEOPLE_SEARCH_ENABLE_TEAM_MAILTO(
+            "peopleSearch.enableTeamMailto", PwmSettingSyntax.BOOLEAN, PwmSettingCategory.PEOPLE_SEARCH ),
+    PEOPLE_SEARCH_ENABLE_PRINTING(
+            "peopleSearch.enablePrinting", PwmSettingSyntax.BOOLEAN, PwmSettingCategory.PEOPLE_SEARCH ),
     PEOPLE_SEARCH_IDLE_TIMEOUT_SECONDS(
             "peopleSearch.idleTimeout", PwmSettingSyntax.DURATION, PwmSettingCategory.PEOPLE_SEARCH ),
     PEOPLE_SEARCH_ENABLE_ADVANCED_SEARCH(

+ 9 - 0
server/src/main/java/password/pwm/http/servlet/peoplesearch/PeopleSearchClientConfigBean.java

@@ -52,8 +52,12 @@ public class PeopleSearchClientConfigBean implements Serializable
     private int orgChartMaxParents;
     private int maxAdvancedSearchAttributes;
     private List<SearchAttribute> advancedSearchAttributes;
+    private boolean enableOrgChartPrinting;
     private boolean enableExport;
     private int exportMaxDepth;
+    private boolean enableMailtoLinks;
+    private int mailtoLinkMaxDepth;
+
 
 
     @Value
@@ -121,9 +125,14 @@ public class PeopleSearchClientConfigBean implements Serializable
                 .orgChartMaxParents( peopleSearchConfiguration.getOrgChartMaxParents() )
 
                 .enableAdvancedSearch( peopleSearchConfiguration.isEnableAdvancedSearch() )
+                .enableOrgChartPrinting( peopleSearchConfiguration.isEnablePrinting() )
+
                 .maxAdvancedSearchAttributes( 3 )
                 .advancedSearchAttributes( searchAttributes )
 
+                .mailtoLinkMaxDepth( peopleSearchConfiguration.getMailtoLinksMaxDepth() )
+                .enableMailtoLinks( peopleSearchConfiguration.isEnableMailtoLinks() )
+
                 .enableExport( peopleSearchConfiguration.isEnableExportCsv() )
                 .exportMaxDepth( peopleSearchConfiguration.getExportCsvMaxDepth() )
 

+ 15 - 0
server/src/main/java/password/pwm/http/servlet/peoplesearch/PeopleSearchConfiguration.java

@@ -127,6 +127,16 @@ public class PeopleSearchConfiguration
         return Integer.parseInt( pwmRequest.getConfig().readAppProperty( AppProperty.PEOPLESEARCH_EXPORT_CSV_MAX_DEPTH ) );
     }
 
+    boolean isEnableMailtoLinks( )
+    {
+        return pwmApplication.getConfig().readSettingAsBoolean( PwmSetting.PEOPLE_SEARCH_ENABLE_TEAM_MAILTO );
+    }
+
+    int getMailtoLinksMaxDepth( )
+    {
+        return Integer.parseInt( pwmRequest.getConfig().readAppProperty( AppProperty.PEOPLESEARCH_EXPORT_CSV_MAX_DEPTH ) );
+    }
+
     TimeDuration getExportCsvMaxDuration( )
     {
         final int seconds = Integer.parseInt( pwmRequest.getConfig().readAppProperty( AppProperty.PEOPLESEARCH_EXPORT_CSV_MAX_SECONDS ) );
@@ -165,6 +175,11 @@ public class PeopleSearchConfiguration
         return ( int ) pwmRequest.getConfig().readSettingAsLong( PwmSetting.PEOPLE_SEARCH_RESULT_LIMIT );
     }
 
+    boolean isEnablePrinting()
+    {
+        return pwmRequest.getConfig().readSettingAsBoolean( PwmSetting.PEOPLE_SEARCH_ENABLE_PRINTING );
+    }
+
     public static PeopleSearchConfiguration forRequest(
             final PwmRequest pwmRequest
     )

+ 29 - 15
server/src/main/java/password/pwm/http/servlet/peoplesearch/PeopleSearchDataReader.java

@@ -76,11 +76,8 @@ import java.util.Map;
 import java.util.Objects;
 import java.util.Set;
 import java.util.TreeMap;
-import java.util.concurrent.ArrayBlockingQueue;
 import java.util.concurrent.Executor;
-import java.util.concurrent.ThreadFactory;
 import java.util.concurrent.ThreadPoolExecutor;
-import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicInteger;
 
 class PeopleSearchDataReader
@@ -929,6 +926,34 @@ class PeopleSearchDataReader
         return storeDataInCache( CacheIdentifier.attributeRead, userIdentity.toDelimitedKey() + "|" + attribute, String.class, cacheLoader );
     }
 
+    public List<String> getMailToLink(
+            final UserIdentity userIdentity,
+            final int depth
+    )
+            throws PwmUnrecoverableException
+    {
+        final List<String> returnValues = new ArrayList<>(  );
+        final String mailtoAttr = userIdentity.getLdapProfile( pwmRequest.getConfig() ).readSettingAsString( PwmSetting.EMAIL_USER_MAIL_ATTRIBUTE );
+        final String value = readUserAttribute( userIdentity, mailtoAttr );
+        if ( !StringUtil.isEmpty( value ) )
+        {
+            returnValues.add( value );
+        }
+
+        if ( depth > 0 )
+        {
+            final OrgChartDataBean orgChartDataBean = this.makeOrgChartData( userIdentity, false );
+            for ( final OrgChartReferenceBean orgChartReferenceBean : orgChartDataBean.getChildren() )
+            {
+                final String userKey = orgChartReferenceBean.getUserKey();
+                final UserIdentity childIdentity = PeopleSearchServlet.readUserIdentityFromKey( pwmRequest, userKey );
+                returnValues.addAll( getMailToLink( childIdentity, depth - 1 ) );
+            }
+        }
+
+        return Collections.unmodifiableList( returnValues );
+    }
+
     void writeUserOrgChartDetailToCsv(
             final CSVPrinter csvPrinter,
             final UserIdentity userIdentity,
@@ -938,17 +963,7 @@ class PeopleSearchDataReader
         final Instant startTime = Instant.now();
         LOGGER.trace( pwmRequest, "beginning csv export starting with user " + userIdentity.toDisplayString() + " and depth of " + depth );
 
-
-        final int threadCount = peopleSearchConfiguration.getExportCsvMaxThreads();
-        final ThreadFactory threadFactory = JavaHelper.makePwmThreadFactory( JavaHelper.makeThreadName( pwmRequest.getPwmApplication(), OrgChartCsvRowOutputJob.class ), true );
-        final ThreadPoolExecutor executor = new ThreadPoolExecutor(
-                threadCount,
-                threadCount,
-                1,
-                TimeUnit.MINUTES,
-                new ArrayBlockingQueue<>( 5000 ),
-                threadFactory
-        );
+        final ThreadPoolExecutor executor = pwmRequest.getPwmApplication().getPeopleSearchService().getJobExecutor();
 
         final AtomicInteger rowCounter = new AtomicInteger( 0 );
         final OrgChartExportState orgChartExportState = new OrgChartExportState(
@@ -963,7 +978,6 @@ class PeopleSearchDataReader
 
         final TimeDuration maxDuration = peopleSearchConfiguration.getExportCsvMaxDuration();
         JavaHelper.pause( maxDuration.asMillis(), 1000, o -> ( executor.getQueue().size() + executor.getActiveCount() <= 0 ) );
-        executor.shutdown();
 
         final TimeDuration timeDuration = TimeDuration.fromCurrent( startTime );
         LOGGER.trace( pwmRequest, "completed csv export of " + rowCounter.get() + " records in " + timeDuration.asCompactString() );

+ 89 - 0
server/src/main/java/password/pwm/http/servlet/peoplesearch/PeopleSearchService.java

@@ -0,0 +1,89 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2018 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
+ */
+
+package password.pwm.http.servlet.peoplesearch;
+
+import password.pwm.PwmApplication;
+import password.pwm.error.PwmException;
+import password.pwm.health.HealthRecord;
+import password.pwm.svc.PwmService;
+import password.pwm.util.java.JavaHelper;
+
+import java.util.List;
+import java.util.concurrent.ArrayBlockingQueue;
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+
+public class PeopleSearchService implements PwmService
+{
+    private PwmApplication pwmApplication;
+    private ThreadPoolExecutor threadPoolExecutor;
+
+    @Override
+    public STATUS status()
+    {
+        return null;
+    }
+
+    @Override
+    public void init( final PwmApplication pwmApplication ) throws PwmException
+    {
+        this.pwmApplication = pwmApplication;
+
+        final int maxThreadCount = 5;
+
+        final ThreadFactory threadFactory = JavaHelper.makePwmThreadFactory( JavaHelper.makeThreadName( pwmApplication, PeopleSearchService.class ), true );
+        threadPoolExecutor = new ThreadPoolExecutor(
+                maxThreadCount,
+                maxThreadCount,
+                1,
+                TimeUnit.MINUTES,
+                new ArrayBlockingQueue<>( 5000 ),
+                threadFactory
+        );
+
+    }
+
+    @Override
+    public void close()
+    {
+        threadPoolExecutor.shutdown();
+    }
+
+    @Override
+    public List<HealthRecord> healthCheck()
+    {
+        return null;
+    }
+
+    @Override
+    public ServiceInfoBean serviceInfo()
+    {
+        return null;
+    }
+
+    public ThreadPoolExecutor getJobExecutor()
+    {
+        return threadPoolExecutor;
+    }
+}

+ 30 - 1
server/src/main/java/password/pwm/http/servlet/peoplesearch/PeopleSearchServlet.java

@@ -53,8 +53,10 @@ import javax.servlet.ServletException;
 import javax.servlet.http.HttpServletResponse;
 import java.io.IOException;
 import java.io.OutputStream;
+import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
+import java.util.List;
 
 public abstract class PeopleSearchServlet extends ControlledPwmServlet
 {
@@ -71,7 +73,8 @@ public abstract class PeopleSearchServlet extends ControlledPwmServlet
         photo( HttpMethod.GET ),
         clientData( HttpMethod.GET ),
         orgChartData( HttpMethod.GET ),
-        exportOrgChart ( HttpMethod.GET ),;
+        exportOrgChart ( HttpMethod.GET ),
+        mailtoLinks ( HttpMethod.GET ),;
 
         private final HttpMethod method;
 
@@ -309,6 +312,32 @@ public abstract class PeopleSearchServlet extends ControlledPwmServlet
         return ProcessStatus.Halt;
     }
 
+    @ActionHandler( action = "mailtoLinks" )
+    private ProcessStatus processMailtoLinksRequest( final PwmRequest pwmRequest )
+            throws PwmUnrecoverableException, IOException
+    {
+        final String userKey = pwmRequest.readParameterAsString( PARAM_USERKEY, PwmHttpRequestWrapper.Flag.BypassValidation );
+        final int requestedDepth = pwmRequest.readParameterAsInt( PARAM_DEPTH, 1 );
+        final UserIdentity userIdentity = readUserIdentityFromKey( pwmRequest, userKey );
+        final PeopleSearchDataReader peopleSearchDataReader = new PeopleSearchDataReader( pwmRequest );
+
+        final PeopleSearchConfiguration peopleSearchConfiguration = PeopleSearchConfiguration.forRequest( pwmRequest );
+
+        if ( !peopleSearchConfiguration.isEnableMailtoLinks() )
+        {
+            final String msg = "mailto links is not enabled.";
+            throw PwmUnrecoverableException.newException( PwmError.ERROR_SERVICE_NOT_AVAILABLE, msg );
+        }
+
+        final int effectiveDepth = Math.max( peopleSearchConfiguration.getMailtoLinksMaxDepth(), requestedDepth );
+        final List<String> mailtoLinks = peopleSearchDataReader.getMailToLink( userIdentity, effectiveDepth );
+
+        pwmRequest.outputJsonResult( RestResultBean.withData( new ArrayList<>( mailtoLinks ) ) );
+
+        return ProcessStatus.Halt;
+    }
+
+
     static UserIdentity readUserIdentityFromKey( final PwmRequest pwmRequest, final String userKey )
             throws PwmUnrecoverableException
     {

+ 1 - 0
server/src/main/java/password/pwm/svc/AbstractPwmService.java

@@ -54,6 +54,7 @@ public abstract class AbstractPwmService
     {
         return startupError;
     }
+
     public final List<HealthRecord> healthCheck( )
     {
         if ( status != PwmService.STATUS.OPEN )

+ 1 - 0
server/src/main/java/password/pwm/svc/PwmServiceEnum.java

@@ -57,6 +57,7 @@ public enum PwmServiceEnum
     SessionTrackService( password.pwm.svc.sessiontrack.SessionTrackService.class ),
     SessionStateSvc( password.pwm.http.state.SessionStateService.class ),
     UserSearchEngine( password.pwm.ldap.search.UserSearchEngine.class, Flag.StartDuringRuntimeInstance ),
+    PeopleSearchService( password.pwm.http.servlet.peoplesearch.PeopleSearchService.class ),
     TelemetryService( password.pwm.svc.telemetry.TelemetryService.class ),
     ClusterService( password.pwm.svc.cluster.ClusterService.class ),
     PwExpiryNotifyService( PwNotifyService.class ),;

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

@@ -252,6 +252,7 @@ peoplesearch.export.csv.maxDepth=1
 peoplesearch.export.csv.maxItems=1000
 peoplesearch.export.csv.maxSeconds=600
 peoplesearch.export.csv.threads=10
+peoplesearch.mailto.maxDepth=1
 peoplesearch.orgChart.enableChildCount=true
 peoplesearch.orgChart.maxParents=50
 peoplesearch.values.verifyUserDN=true

+ 10 - 0
server/src/main/resources/password/pwm/config/PwmSetting.xml

@@ -3149,6 +3149,16 @@
             <value>false</value>
         </default>
     </setting>
+    <setting hidden="false" key="peopleSearch.enableTeamMailto" level="1">
+        <default>
+            <value>false</value>
+        </default>
+    </setting>
+    <setting hidden="false" key="peopleSearch.enablePrinting" level="1">
+        <default>
+            <value>false</value>
+        </default>
+    </setting>
     <setting hidden="false" key="peopleSearch.queryMatch" level="1" required="true">
         <ldapPermission actor="self_other" access="read"/>
         <default syntaxVersion="2">

+ 4 - 0
server/src/main/resources/password/pwm/i18n/PwmSetting.properties

@@ -587,8 +587,10 @@ Setting_Description_peopleSearch.displayName.cardLabels=Specify the display labe
 Setting_Description_peopleSearch.displayName.user=Specify the display name for userDN type records.  Use macros to control the presentation such as the LDAP attribute macro <code>@LDAP\:givenName@</code>.
 Setting_Description_peopleSearch.enable=Enable this option to enable the People Search module.
 Setting_Description_peopleSearch.enableExport=Enable this option to allow download of organizational chart data.
+Setting_Description_peopleSearch.enableTeamMailto=Enable this option to allow to show a link that will email a team of users in the orgchart view.
 Setting_Description_peopleSearch.enableOrgChart=Enable this option to show an organizational chart of users.
 Setting_Description_peopleSearch.enablePublic=Enable this option to allow access to the People Search module for unauthenticated users.
+Setting_Description_peopleSearch.enablePrinting=Enable this option to show a print option in the org chart view.
 Setting_Description_peopleSearch.idleTimeout=Specify the number of seconds after which an authenticated session becomes unauthenticated.   If the value is set to 0, then @PwmAppName@ uses then the system-wide idle timeout value.  If a user is using the People Search module without authenticating, then the system does not apply a timeout.
 Setting_Description_peopleSearch.maxCacheSeconds=Specify the number of seconds that @PwmAppName@ caches the results of searches and record details that it reads from eDirectory. Use this setting to control the maximum amount of time @PwmAppName@ can use cached data. Setting to zero disables the cache entirely, but this might negatively impact the scalability of the application and the LDAP directory.
 Setting_Description_peopleSearch.orgChart.assistantAttribute=Specify the attribute that contains the LDAP DN of the assistant for a user.  If this setting is blank, @PwmAppName@ will not show the assistant on the organizational chart view.
@@ -1100,8 +1102,10 @@ Setting_Label_peopleSearch.displayName.cardLabels=Person Detail Display Labels
 Setting_Label_peopleSearch.displayName.user=UserDN Name Display
 Setting_Label_peopleSearch.enable=Enable People Search
 Setting_Label_peopleSearch.enableExport=Enable Export
+Setting_Label_peopleSearch.enableTeamMailto=Enable Team Mailto
 Setting_Label_peopleSearch.enableOrgChart=Enable Organizational Chart
 Setting_Label_peopleSearch.enablePublic=Enable People Search Public (Non-Authenticated) Access
+Setting_Label_peopleSearch.enablePrinting=Enable Printing
 Setting_Label_peopleSearch.idleTimeout=Idle Timeout Seconds
 Setting_Label_peopleSearch.maxCacheSeconds=Maximum Cache Seconds
 Setting_Label_peopleSearch.orgChart.assistantAttribute=Organizational Assistant Attribute

+ 1 - 2
webapp/src/main/webapp/WEB-INF/jsp/helpdesk.jsp

@@ -30,7 +30,6 @@
 <head>
     <%@ include file="/WEB-INF/jsp/fragment/header-common.jsp" %>
     <link rel="stylesheet" type="text/css" href="<pwm:url url='/public/resources/webjars/pwm-client/vendor/ux-ias/ias-icons.css' addContext="true"/>"/>
-    <link rel="stylesheet" type="text/css" href="<pwm:url url='/public/resources/webjars/pwm-client/vendor/ux-ias/ux-ias.css' addContext="true"/>"/>
 </head>
 <body class="nihilo">
 <div id="wrapper" class="helpdesk-wrapper">
@@ -38,7 +37,7 @@
         <jsp:param name="pwm.PageName" value="Title_Helpdesk"/>
     </jsp:include>
     <div id="centerbody" class="wide tall">
-        <ui-view id="helpdesk-view"><div class="WaitDialogBlank"></div></ui-view>
+        <ui-view id="helpdesk-view" class="ias-styles-root"><div class="WaitDialogBlank"></div></ui-view>
 
         <noscript>
             <span><pwm:display key="Display_JavascriptRequired"/></span>

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

@@ -27,7 +27,6 @@
 <head>
     <%@ include file="/WEB-INF/jsp/fragment/header-common.jsp" %>
     <link rel="stylesheet" type="text/css" href="<pwm:url url='/public/resources/webjars/pwm-client/vendor/ux-ias/ias-icons.css' addContext="true"/>"/>
-    <link rel="stylesheet" type="text/css" href="<pwm:url url='/public/resources/webjars/pwm-client/vendor/ux-ias/ux-ias.css' addContext="true"/>"/>
 </head>
 <body class="nihilo printable">
 <div id="wrapper" class="peoplesearch-wrapper">
@@ -37,7 +36,7 @@
     <div id="centerbody" class="wide tall" style="height:100%">
         <%@ include file="/WEB-INF/jsp/fragment/message.jsp" %>
 
-        <ui-view id="people-search-view"><div class="WaitDialogBlank"></div></ui-view>
+        <ui-view id="people-search-view" class="ias-styles-root"><div class="WaitDialogBlank"></div></ui-view>
     </div>
     <div class="push"></div>
 </div>

Неке датотеке нису приказане због велике количине промена