Browse Source

Reponsive layout. OrgChart responsiveness down to mobile browser widths. Added PersonCard size 'small'. Normalized mock data.

Joe Hawkins 8 years ago
parent
commit
567a9c20f0
26 changed files with 728 additions and 375 deletions
  1. BIN
      src/main/angular/images/question_mark.png
  2. 10 1
      src/main/angular/index.html
  3. 1 0
      src/main/angular/src/component.ts
  4. 1 1
      src/main/angular/src/main.dev.ts
  5. 1 1
      src/main/angular/src/main.ts
  6. 0 2
      src/main/angular/src/models/person.model.ts
  7. 42 32
      src/main/angular/src/peoplesearch/orgchart.component.html
  8. 197 200
      src/main/angular/src/peoplesearch/orgchart.component.scss
  9. 66 5
      src/main/angular/src/peoplesearch/orgchart.component.ts
  10. 4 4
      src/main/angular/src/peoplesearch/peoplesearch-cards.component.html
  11. 16 10
      src/main/angular/src/peoplesearch/peoplesearch-cards.component.scss
  12. 27 9
      src/main/angular/src/peoplesearch/peoplesearch-table.component.scss
  13. 7 5
      src/main/angular/src/peoplesearch/peoplesearch.component.html
  14. 4 42
      src/main/angular/src/peoplesearch/peoplesearch.component.scss
  15. 4 1
      src/main/angular/src/peoplesearch/peoplesearch.module.ts
  16. 9 0
      src/main/angular/src/peoplesearch/peoplesearch.scss
  17. 32 7
      src/main/angular/src/peoplesearch/person-card.component.html
  18. 123 28
      src/main/angular/src/peoplesearch/person-card.component.scss
  19. 27 2
      src/main/angular/src/peoplesearch/person-card.component.ts
  20. 94 24
      src/main/angular/src/services/people.data.json
  21. 12 0
      src/main/angular/src/ux/app-bar.component.scss
  22. 9 0
      src/main/angular/src/ux/app-bar.component.ts
  23. 22 0
      src/main/angular/src/ux/icon-button.component.scss
  24. 7 0
      src/main/angular/src/ux/icon-button.component.ts
  25. 11 0
      src/main/angular/src/ux/ux.module.ts
  26. 2 1
      src/main/angular/webpack.common.js

BIN
src/main/angular/images/question_mark.png


+ 10 - 1
src/main/angular/index.html

@@ -2,10 +2,19 @@
 <html lang="en">
 <head>
     <meta charset="UTF-8">
+    <meta name="viewport" content="initial-scale=1, maximum-scale=1">
     <title>SSPR Development</title>
+
+    <style>
+        html, body {
+            margin: 0;
+            padding: 0;
+            width: 100%;
+        }
+    </style>
 </head>
 <body ng-cloak>
-<ui-view id="people-search-view">Loading...</ui-view>
+<ui-view>Loading...</ui-view>
 
 <script src="vendor/angular.js"></script>
 <script src="vendor/angular-ui-router.js"></script>

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

@@ -5,6 +5,7 @@ export function Component(options: {
     controllerAs?: string,
     template?: string,
     templateUrl?: string,
+    transclude?: boolean,
     stylesheetUrl?: string
 }) {
     return (controller: Function) => angular.extend(options, { controller: controller });

+ 1 - 1
src/main/angular/src/main.dev.ts

@@ -6,7 +6,7 @@ import PeopleService from './services/people.service.dev';
 
 module('app', [
     uiRouter,
-    peopleSearchModule
+    peopleSearchModule,
 ])
 
     .config(routes)

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

@@ -6,7 +6,7 @@ import PeopleService from './services/people.service';
 
 module('app', [
     uiRouter,
-    peopleSearchModule
+    peopleSearchModule,
 ])
 
     .config(routes)

+ 0 - 2
src/main/angular/src/models/person.model.ts

@@ -5,7 +5,6 @@ export default class Person {
     // Details properties (not available in search)
     detail: any;
     displayNames: string[];
-    orgChartParentKey: string;
     photoURL: string;
 
     // Search properties (not available in details)
@@ -22,7 +21,6 @@ export default class Person {
         // Details properties
         this.detail = options.detail;
         this.displayNames = options.displayNames;
-        this.orgChartParentKey = options.orgChartParentKey;
         this.photoURL = options.photoURL;
 
         // Search properties

+ 42 - 32
src/main/angular/src/peoplesearch/orgchart.component.html

@@ -1,41 +1,51 @@
-<div id="page-content-title">Organization</div>
-<div id="orgchart-close" ng-click="$ctrl.close()"></div>
+<app-bar>
+    <div id="page-content-title">Organization</div>
+    <span flex></span>
+    <icon-button ng-click="$ctrl.close()"></icon-button>
+</app-bar>
 
-<div id="orgchart-content" class="ng-cloak">
-    <div class="orgchart-primary-person-connector" class="ng-cloak"></div>
-
-    <div class="orgchart-management-title orgchart-title">Management</div>
-    <div class="orgchart-management" ng-if="$ctrl.managementChain.length" >
-        <div class="orgchart-manager" ng-repeat="manager in $ctrl.managementChain"
-             ng-click="$ctrl.selectPerson(manager.userKey)">
-            <div class="orgchart-separator" ng-class="{first:$first,last:$last}"></div>
-            <div class="orgchart-picture">
-                <img ng-src="{{ manager.photoURL }}" />
-            </div>
-            <div class="orgchart-manager-fields">
-                <div class="orgchart-field orgchart-field-0">{{ manager.displayNames[0] }}</div>
-                <div class="orgchart-field orgchart-field-1">{{ manager.displayNames[1] }}</div>
-            </div>
+<div class="org-chart-section managers" ng-if="$ctrl.hasManagementChain() || $ctrl.isOrphan()">
+    <h3>Management</h3>
+    <div ng-if="!$ctrl.isOrphan()">
+        <div class="manager"
+             ng-repeat="manager in $ctrl.getManagementChain()"
+             ng-switch="$ctrl.getManagerCardSize()">
+            <div class="org-chart-connector"></div>
+            <person-card person="manager"
+                         size="small"
+                         ng-click="$ctrl.selectPerson(manager.userKey)" ng-switch-when="small">
+            </person-card>
+            <person-card person="manager"
+                         ng-click="$ctrl.selectPerson(manager.userKey)" ng-switch-default>
+            </person-card>
+        </div>
+    </div>
+    <div ng-if="$ctrl.isOrphan()">
+        <div class="manager empty-manager"
+             ng-switch="$ctrl.getManagerCardSize()">
+            <div class="org-chart-connector"></div>
+            <person-card person="$ctrl.emptyPerson" size="small" ng-switch-when="small"></person-card>
+            <person-card person="$ctrl.emptyPerson" ng-switch-default></person-card>
         </div>
     </div>
+</div>
 
-    <div class="orgchart-primary-person">
-        <img class="orgchart-picture" ng-src="{{ $ctrl.person.photoURL }}" />
-        <div class="orgchart-num-reports" ng-if="$ctrl.directReports.length">{{ $ctrl.directReports.length }}</div>
+<div class="org-chart-section">
+    <person-card person="$ctrl.person" direct-reports="$ctrl.directReports" size="large"></person-card>
+</div>
 
-        <div class="orgchart-primary-field orgchart-field-0">{{ $ctrl.person.displayNames[0] }}</div>
-        <div class="orgchart-primary-field orgchart-field orgchart-field-1">{{ $ctrl.person.displayNames[1] }}</div>
-        <div class="orgchart-primary-field orgchart-field orgchart-field-2">{{ $ctrl.person.displayNames[2] }}</div>
-        <div class="orgchart-primary-field orgchart-field orgchart-field-3">{{ $ctrl.person.displayNames[3] }}</div>
-        <div class="orgchart-primary-field orgchart-field orgchart-field-4">{{ $ctrl.person.displayNames[4] }}</div>
-        <div class="orgchart-primary-field orgchart-field orgchart-field-5">{{ $ctrl.person.displayNames[5] }}</div>
-        <div class="orgchart-primary-field orgchart-field orgchart-field-6">{{ $ctrl.person.displayNames[6] }}</div>
-    </div>
+<div class="org-chart-section direct-reports" ng-if="$ctrl.hasDirectReports()">
+    <h3>Direct Reports</h3>
+    <div class="org-chart-connector"></div>
 
-    <div class="orgchart-direct-reports-title orgchart-title">Direct Reports</div>
-    <div class="orgchart-direct-reports person-card-list" ng-if="$ctrl.directReports.length">
-        <person-card person="directReport" ng-repeat="directReport in $ctrl.directReports"
-            ng-click="$ctrl.selectPerson(directReport.userKey)">
+    <div class="person-card-list">
+        <person-card person="directReport"
+                     ng-repeat="directReport in $ctrl.directReports"
+                     ng-click="$ctrl.selectPerson(directReport.userKey)">
         </person-card>
     </div>
+
+    <div class="person-card-list" ng-if="!$ctrl.hasDirectReports()">
+        <span class="empty-section-label">0 direct reports</span>
+    </div>
 </div>

+ 197 - 200
src/main/angular/src/peoplesearch/orgchart.component.scss

@@ -1,235 +1,232 @@
-org-chart {
-  bottom: 0;
-  left: 0;
-  position: absolute;
-  right: 0;
-  top: 0;
-}
+$manager-connector-height: 16px;
 
-#orgchart-content {
-  bottom: 0;
-  left: 0;
-  overflow: auto;
-  position: absolute;
-  right: 0;
-  top: 42px;
-}
-
-#orgchart-close {
-  background: gray no-repeat scroll 0 0 / 20px 20px;
-  border: 1px solid transparent;
-  height: 20px;
-  position: absolute;
-  right: 0;
-  top: 0;
-  width: 20px;
-
-  &:hover {
-    background-color: #f6f9f8;
-    border: 1px solid #dae1e1;
-    color: #0088ce;
-  }
-}
-
-.orgchart-direct-reports {
-  border-top: 3px solid #808080;
-  margin-right: 25px;
-  margin-top: 40px;
-  min-width: 700px;
-  padding-top: 0;
-
-  .num-reports {
-    position: absolute;
-    right: 3px;
-    top: 3px;
-  }
-}
-
-.orgchart-direct-reports-title {
-  top: 315px;
-}
+// Default display
+org-chart {
+  display: block;
+  min-width: 300px;
 
-.orgchart-num-reports {
-  background-color: #dae1e1;
-  border-radius: 2px;
-  color: #434c50;
-  font-size: 12px;
-  height: 18px;
-  line-height: 18px;
-  margin-bottom: 0;
-  padding-bottom: 0;
-  position: absolute;
-  right: 3px;
-  text-align: center;
-  top: 3px;
-  width: 25px;
-}
+  > .org-chart-section {
+    position: relative;
+    text-align: center;
 
-.orgchart-primary-person {
-  background: #fff none repeat scroll 0 0;
-  border: 3px solid #808080;
-  border-radius: 3px;
-  float: none;
-  height: 160px;
-  margin: 20px 0 0 120px;
-  position: relative;
-  width: 340px;
-
-  .orgchart-picture {
-    background: gray repeat scroll 0 0 / 65px 65px;
-    height: 65px;
-    left: 10px;
-    position: absolute;
-    top: 10px;
-    width: 65px;
-  }
+    &.direct-reports {
+      > .org-chart-connector {
+        height: 34px;
+      }
+    }
 
-  .orgchart-primary-field {
-    color: #808080;
-    left: 90px;
-    font-size: 13px;
-    position: absolute;
-  }
+    &.managers {
+      min-height: 98px;
+
+      .manager {
+        margin-bottom: $manager-connector-height;
+        position: relative;
+        text-align: center;
+
+        &.empty-manager {
+          > person-card {
+            cursor: initial;
+
+            > .person-card-content {
+              > .avatar {
+                background-color: #dae1e1;
+                border-color: #dae1e1;
+              }
+            }
+          }
+
+          > .org-chart-connector {
+            background-color: #dae1e1;
+          }
+        }
+
+        > person-card {
+          display: inline-block;
+        }
+      }
 
-  .orgchart-primary-field,
-  .orgchard-field-value {
-    &.link {
-      color: #0088ce;
+      .org-chart-connector {
+        bottom: -$manager-connector-height;
+        height: $manager-connector-height;
+        top: initial;
+      }
     }
-  }
 
+    > h3 {
+      color: #808080;
+      font-size: 14px;
+      line-height: 14px;
+      margin: 0;
+      padding: 15px 0 5px 0;
+      text-align: left;
+    }
 
-  .orgchart-field-0 {
-    color: black;
-    font-size: 14px;
-    top: 10px;
-  }
-
-  .orgchart-field-1 {
-    top: 28px;
-  }
+    > person-card {
+      &[size="large"] {
+        margin: 0 auto;
+      }
+    }
 
-  .orgchart-field-2 {
-    top: 46px;
-  }
+    > .person-card-list {
+      border-top: 3px solid #808080;
+      min-height: 90px;
+      padding-top: 5px;
 
-  .orgchart-secondary-field {
-    position: absolute;
-    left: 10px;
+      > person-card {
+        display: inline-block;
+        width: 100%;
 
-    .orgchart-field-name {
-      color: #808080;
-      display: inline-block;
-      font-size: 13px;
-      overflow: hidden;
-      width: 100px;
+        &:not(:last-child) {
+          margin-bottom: 5px;
+        }
+      }
     }
 
-    .orgchart-field-value {
-      display: inline-block;
-      font-size: 13px;
-      overflow: hidden;
-      width: 215px;
+    .org-chart-connector {
+      background-color: #808080;
+      left: 0;
+      margin: 0 auto;
+      position: absolute;
+      right: 0;
+      top: 0;
+      width: 5px;
     }
-  }
-
-  .orgchart-field-3 {
-    border-top: 1px solid #dae1e1;
-    padding-top: 8px;
-    top: 80px;
-  }
 
-  .orgchart-field-4 {
-    top: 111px;
+    .empty-section-label {
+      color: #808080;
+    }
   }
+}
 
-  .orgchart-field-5 {
-    top: 134px;
+// Too wide for full width person-card
+@media (min-width: 375px) {
+  org-chart {
+    > .org-chart-section {
+      > .person-card-list {
+        > person-card {
+          margin-right: 5px;
+          width: 200px;
+        }
+      }
+    }
   }
 }
 
-.orgchart-management {
-  margin-left: 90px;
-  min-width: 650px;
-
-  .orgchart-manager {
-    display: inline-block;
-    padding-top: 75px;
-    position: relative;
-    width: 130px;
+// Wide enough to fit multiple person-cards next to each other inline
+@media (min-width: 420px) {
+  org-chart {
+    > .org-chart-section {
+      > .person-card-list {
+        text-align: left;
+      }
 
-    .orgchart-separator {
-      background-color: #dae1e1;
-      height: 3px;
-      position: absolute;
-      top: 40px;
-      width: 100%;
+      &.managers {
+        .manager {
+          text-align: center;
+        }
+      }
     }
+  }
+}
 
-    .orgchart-separator {
-      &.first {
-        margin-left: 50%;
-        width: 50%;
-      }
+// Wide enough to show main person offset to the right. Manager should now be locked in place (instead of centered)
+@media (min-width: 464px) {
+  org-chart {
+    > .org-chart-section {
+      text-align: left;
 
-      &.last {
-        margin-right: 50%;
-        width: 50%;
+      > person-card {
+        &[size="large"] {
+          margin: 0 0 0 128px;
+        }
       }
 
-      &.first.last {
-        display: none;
+      .org-chart-connector {
+        left: 169px;
+        margin: 0;
       }
-    }
 
-    .orgchart-field {
-      color: #808080;
-      font-size: 12px;
-      text-align: center;
-      overflow: hidden;
-      text-overflow: ellipsis;
-      white-space: nowrap;
-    }
-
-    .orgchart-picture {
-      left: 35px;
-      margin: 0 auto;
-      position: absolute;
-      top: 10px;
-      width: 50%;
-    }
+      &.managers {
+        .org-chart-connector {
+          left: 34px;
+        }
 
-    .orgchart-picture img {
-      border: 3px solid #dae1e1;
-      border-radius: 50%;
-      height: 50px;
-      margin: 3px;
-      width: 50px;
+        .manager {
+          display: block;
+          margin-left: 135px;
+          text-align: left;
+        }
+      }
     }
   }
 }
 
-.orgchart-manager-fields {
-  background-color: white;
-  padding: 4px;
-}
-
-.orgchart-management-title {
-  top: 25px;
-}
-
-.orgchart-primary-person-connector {
-  background-color: #808080;
-  height: 290px;
-  left: 155px;
-  position: absolute;
-  top: 50px;
-  width: 5px;
-}
+// Wide enough to display several managers horizontally
+@media (min-width: 625px) {
+  org-chart {
+    > .org-chart-section {
+      &.managers {
+        margin-left: 0;
+        min-height: 162px;
+        overflow: hidden;
+        white-space: nowrap;
+
+        .org-chart-connector {
+          left: 42px;
+        }
+
+        .manager {
+          display: inline-block;
+          margin-left: 0;
+          margin-bottom: 32px;
+
+          &:first-child {
+            margin-left: 115px;
+
+            > .org-chart-connector {
+              bottom: initial;
+              top: 56px;
+              left: 57px;
+              height: 72px;
+            }
+          }
+
+          &:not(:first-child) {
+            > .org-chart-connector {
+              background-color: #dae1e1;
+              bottom: initial;
+              height: 3px;
+              left: -37px;
+              top: 26px;
+              width: 69px;
+            }
+
+            > person-card {
+              > .person-card-content {
+                > .avatar {
+                  &:not(:hover) {
+                    border-color: #dae1e1;
+                  }
+                }
+
+                > .details {
+                  > :first-child {
+                    color: #808080;
+                  }
+                }
+              }
+            }
+          }
+
+          &:not(:last-child) {
+            margin-right: 5px;
+          }
+        }
+      }
 
-.orgchart-title {
-  color: #808080;
-  font-size: 14px;
-  left: 0;
-  position: absolute;
+      .org-chart-connector {
+        left: 172px;
+      }
+    }
+  }
 }

+ 66 - 5
src/main/angular/src/peoplesearch/orgchart.component.ts

@@ -1,7 +1,8 @@
 import { Component } from '../component';
-import { IQService } from 'angular';
+import { element, IQService, IWindowService } from 'angular';
 import PeopleService from '../services/people.service';
 import Person from '../models/person.model';
+import { IScope } from 'angular';
 
 @Component({
     stylesheetUrl: require('peoplesearch/orgchart.component.scss'),
@@ -9,20 +10,26 @@ import Person from '../models/person.model';
 })
 export default class OrgChartComponent {
     private person: Person;
+    private emptyPerson: Person;
     private managementChain: Person[];
     private directReports: Person[];
+    private windowWidth: number;
 
-    static $inject = ['$q', '$state', '$stateParams', 'PeopleService'];
+    static $inject = ['$q', '$scope', '$state', '$stateParams', '$window', 'PeopleService'];
     constructor(
         private $q: IQService,
+        private $scope: IScope,
         private $state: angular.ui.IStateService,
         private $stateParams: angular.ui.IStateParamsService,
+        private $window: IWindowService,
         private peopleService: PeopleService) {
     }
 
     $onInit() {
         var personId: string = this.$stateParams['personId'];
+        var self = this;
 
+        // Fetch data
         if (personId) {
             this.$q.all({
                 directReports: this.peopleService.getDirectReports(personId),
@@ -30,21 +37,75 @@ export default class OrgChartComponent {
                 person: this.peopleService.getPerson(personId)
             })
             .then((data) => {
-                this.directReports = data['directReports'];
-                this.managementChain = data['managementChain'];
-                this.person = data['person'];
+                this.$scope.$evalAsync(() => {
+                    self.directReports = data['directReports'];
+                    self.managementChain = data['managementChain'];
+                    self.person = data['person'];
+                });
             })
             .catch((result) => {
                 console.log(result);
             });
         }
+
+        // Setup listener on window resize
+        // noinspection TypeScriptUnresolvedFunction
+        element(this.$window).bind('resize', function() {
+            self.setWindowWidth();
+            self.$scope.$apply();
+        });
+
+        // Initialize windowWidth
+        this.setWindowWidth();
+
+        this.emptyPerson = new Person({
+            displayNames: [
+                'No Managers'
+            ],
+            photoURL: 'images/question_mark.png',
+            userKey: null
+        });
     }
 
     close() {
         this.$state.go('search.table');
     }
 
+    getManagerCardSize() {
+        return this.isWideLayout() ? 'small' : 'normal';
+    }
+
+    getManagementChain(): Person[] {
+        if (this.isWideLayout()) {
+            // return original data
+            return this.managementChain;
+        }
+
+        // return reversed data
+        return [].concat(this.managementChain).reverse();
+    }
+
+    hasDirectReports() {
+        return this.directReports && this.directReports.length;
+    }
+
+    hasManagementChain() {
+        return this.managementChain && this.managementChain.length;
+    }
+
+    isOrphan() {
+        return !(this.hasDirectReports() || this.hasManagementChain());
+    }
+
     selectPerson(userKey: string) {
         this.$state.go('orgchart', { personId: userKey });
     }
+
+    setWindowWidth() {
+        this.windowWidth = this.$window.innerWidth;
+    }
+
+    private isWideLayout() {
+        return this.windowWidth >= 625;
+    }
 }

+ 4 - 4
src/main/angular/src/peoplesearch/peoplesearch-cards.component.html

@@ -1,4 +1,4 @@
-<div class="person-card-list">
-    <person-card person="person" ng-repeat="person in $ctrl.people" ng-click="$ctrl.selectPerson(person.userKey)">
-    </person-card>
-</div>
+<person-card person="person"
+             ng-repeat="person in $ctrl.people"
+             ng-click="$ctrl.selectPerson(person.userKey)">
+</person-card>

+ 16 - 10
src/main/angular/src/peoplesearch/peoplesearch-cards.component.scss

@@ -1,15 +1,21 @@
 people-search-cards {
-    bottom: 0;
-    left: 0;
-    overflow: auto;
-    position: absolute;
-    right: 0;
-    top: 0;
-}
+    text-align: center;
 
-.person-card-list {
     > person-card {
-        margin-right: 5px;
-        margin-top: 5px;
+        display: inline-block;
+        width: 100%;
+
+        &:not(:last-child) {
+            margin-bottom: 5px;
+        }
     }
 }
+
+@media (min-width: 375px) {
+    people-search-cards {
+        > person-card {
+            margin-right: 5px;
+            width: 200px;
+        }
+    }
+}

+ 27 - 9
src/main/angular/src/peoplesearch/peoplesearch-table.component.scss

@@ -1,18 +1,27 @@
 people-search-table {
-    border: 1px solid #dae1e1;
-    bottom: 0;
-    left: 0;
-    overflow: auto;
-    position: absolute;
-    right: 0;
-    top: 0;
+    display: flex;
+    flex-flow: column nowrap;
+    overflow-x: auto;
 
     > table {
-        border: 0 none;
+        border: 1px solid #dae1e1;
+        border-collapse: collapse;
+
+        flex: 1 1;
+        margin-top: 10px;
         width: 100%;
 
+        > tbody {
+            > tr {
+                cursor: pointer;
+
+                &:hover {
+                    background-color: #eeeeee;
+                }
+            }
+        }
+
         th, td {
-            border-bottom: 1px solid #dae1e1;
             font-weight: normal;
             overflow: hidden;
             padding: 5px;
@@ -24,6 +33,15 @@ people-search-table {
             background-color: #eeeeee;
             color: #697c87;
         }
+
+        tr {
+            &:not(:last-child) {
+                > th,
+                > td {
+                    border-bottom: 1px solid #dae1e1;
+                }
+            }
+        }
     }
 }
 

+ 7 - 5
src/main/angular/src/peoplesearch/peoplesearch.component.html

@@ -1,6 +1,10 @@
-<div id="page-content-title">People Search</div>
+<app-bar>
+    <div id="page-content-title">People Search</div>
+    <span flex></span>
+    <icon-button ng-click="$ctrl.viewToggleClicked()" ng-class="$ctrl.viewToggleClass"></icon-button>
+</app-bar>
 
-<div id="panel-searchbar" class="searchbar">
+<div class="search-bar">
     <input id="username" name="username" placeholder="Search" class="peoplesearch-input-username"
            autocomplete="off" ng-model="$ctrl.query" /> <!-- Auto focus this control -->
     <div class="searchbar-extras">
@@ -14,6 +18,4 @@
     </div>
 </div>
 
-<div id="people-search-view-toggle" ng-click="$ctrl.viewToggleClicked()" ng-class="$ctrl.viewToggleClass"></div>
-
-<ui-view id="people-search-component-view">Loading...</ui-view>
+<ui-view>Loading...</ui-view>

+ 4 - 42
src/main/angular/src/peoplesearch/peoplesearch.component.scss

@@ -1,43 +1,5 @@
-#people-search-component-view {
-  bottom: 0;
-  left: 0;
-  position: absolute;
-  right: 0;
-  top: 85px;
-}
-
-#people-search-view {
-  bottom: 0;
-  display: block;
-  left: 0;
-  margin: 5px;
-  position: absolute;
-  right: 0;
-  top: 0;
-}
-
-#people-search-view-toggle {
-  border: 1px solid transparent;
-  color: #808080;
-  cursor: pointer;
-  font-size: 20px;
-  height: 24px;
-  position: absolute;
-  right: 0;
-  top: 42px;
-  width: 24px;
-
-  &:hover {
-    background-color: #f6f9f8;
-    border: 1px solid #dae1e1;
-    color: #0088ce;
+people-search {
+  > .search-bar {
+    margin-bottom: 10px;
   }
-
-  &.fa {
-    &::before {
-      left: 2px;
-      position: absolute;
-      top: 2px;
-    }
-  }
-}
+}

+ 4 - 1
src/main/angular/src/peoplesearch/peoplesearch.module.ts

@@ -5,10 +5,13 @@ import PeopleSearchComponent from './peoplesearch.component';
 import PeopleSearchTableComponent from './peoplesearch-table.component';
 import PeopleSearchCardsComponent from './peoplesearch-cards.component';
 import PersonCardComponent from './person-card.component';
+import uxModule from '../ux/ux.module';
+
+require('./peoplesearch.scss');
 
 var moduleName = 'people-search';
 
-module(moduleName, [ ])
+module(moduleName, [ uxModule ])
     .service('PeopleSearchService', PeopleSearchService)
     .component('orgChart', OrgChartComponent)
     .component('personCard', PersonCardComponent)

+ 9 - 0
src/main/angular/src/peoplesearch/peoplesearch.scss

@@ -0,0 +1,9 @@
+body {
+  > ui-view {
+    box-sizing: border-box;
+    display: block;
+    overflow: hidden;
+    padding: 5px;
+    width: 100%;
+  }
+}

+ 32 - 7
src/main/angular/src/peoplesearch/person-card.component.html

@@ -1,7 +1,32 @@
-<div class="person-card-image"></div>
-<div class="person-card-details">
-    <div class="person-card-row-1" ng-bind="$ctrl.data[0]"></div>
-    <div class="person-card-row-2" ng-bind="$ctrl.data[1]"></div>
-    <div class="person-card-row-3" ng-bind="$ctrl.data[2]"></div>
-    <div class="person-card-row-4" ng-bind="$ctrl.data[3]"></div>
-</div>
+<div class="person-card-content" ng-switch="$ctrl.size">
+    <div class="avatar">
+        <img ng-src="{{ $ctrl.person.photoURL }}" alt="User photo">
+    </div>
+    <div class="reports" ng-if="$ctrl.directReports.length" ng-bind="$ctrl.directReports.length"></div>
+
+    <div class="details" ng-switch-when="small">
+        <div ng-bind="$ctrl.person.displayNames[0]"></div>
+        <div ng-bind="$ctrl.person.displayNames[1]"></div>
+    </div>
+
+    <div class="details" ng-switch-when="large">
+        <div ng-bind="$ctrl.person.displayNames[0]"></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="secondary-details">
+            <div ng-bind="$ctrl.person.displayNames[4]"></div>
+            <div ng-bind="$ctrl.person.displayNames[5]"></div>
+            <div ng-bind="$ctrl.person.displayNames[6]"></div>
+            <div ng-bind="$ctrl.person.displayNames[7]"></div>
+        </div>
+    </div>
+
+    <div class="details" ng-switch-default>
+        <div ng-bind="$ctrl.data[0]"></div>
+        <div ng-bind="$ctrl.data[1]"></div>
+        <div ng-bind="$ctrl.data[2]"></div>
+        <div ng-bind="$ctrl.data[3]"></div>
+    </div>
+</div>

+ 123 - 28
src/main/angular/src/peoplesearch/person-card.component.scss

@@ -4,20 +4,24 @@ $text-color-subtext: #808080;
 $person-card-bg-color: #eef2f2;
 $person-card-hover-bg-color: #f6f9f8;
 $person-card-border-color: #28a9e1;
-$person-card-height: 60px;
+$person-card-height: 82px;
 $person-card-width: 200px;
-$person-card-image-size: 50px;
+$person-card-avatar-size: 50px;
 $person-card-spacing: 10px;
 
+$person-card-large-avatar-size: 65px;
+
 person-card {
   background-color: $person-card-bg-color;
   border: 1px solid $person-card-bg-color;
+  border-radius: 3px;
+  box-sizing: border-box;
   cursor: pointer;
-  display: inline-flex;
-  flex-flow: row nowrap;
+  display: block;
   height: $person-card-height;
-  overflow: hidden;
   padding: $person-card-spacing;
+  position: relative;
+  text-align: left;
   vertical-align: top;
   width: $person-card-width;
 
@@ -26,39 +30,130 @@ person-card {
     border-color: $person-card-border-color;
   }
 
-  > .person-card-image {
-    background-color: gray;
-    flex: 0 0 $person-card-image-size;
-    height: $person-card-image-size;
-    margin-right: $person-card-spacing;
-    width: $person-card-image-size;
+  &[size="large"] {
+    background-color: #ffffff;
+    border: 3px solid #808080;
+    border-radius: 3px;
+    height: 166px;
+    width: 326px;
+    max-width: 100%;
+    min-width: 310px;
+
+    > .person-card-content {
+      flex-flow: row wrap;
+
+      > .avatar {
+        flex: 0 0 $person-card-large-avatar-size;
+        height: $person-card-large-avatar-size;
+        margin-right: 15px;
+        width: $person-card-large-avatar-size;
 
-    img {
-      height: 100%;
-      width: 100%;
+        img {
+          height: 100%;
+          width: 100%;
+        }
+      }
     }
   }
 
-  > .person-card-details {
-    flex-grow: 1;
-    width: $person-card-width - $person-card-image-size;
+  &[size="small"] {
+    border: none;
+    background-color: transparent;
+    height: 96px;
+    padding: 0;
+    width: 120px;
+
+    > .person-card-content {
+      display: block;
+      text-align: center;
+
+      > .avatar {
+        border: 3px solid #808080;
+        border-radius: 100%;
+        height: $person-card-avatar-size;
+        margin: 0 auto;
+        width: $person-card-avatar-size;
 
-    > div {
-      overflow: hidden;
-      text-overflow: ellipsis;
-      white-space: nowrap;
+        > img {
+          border-radius: 100%;
+        }
+
+        &:hover {
+          border-color: $person-card-border-color;
+        }
+      }
+
+      > .details {
+        background-color: white;
+        margin-top: 8px;
+        width: 100%;
+
+        :first-child {
+          font-size: 13px;
+          line-height: 13px;
+        }
+      }
     }
+  }
+
+  > .person-card-content {
+    display: flex;
+    flex-flow: row nowrap;
+    overflow: hidden;
+
+    > .avatar {
+      background-color: gray;
+      flex: 0 0 $person-card-avatar-size;
+      height: $person-card-avatar-size;
+      margin-right: $person-card-spacing;
+      width: $person-card-avatar-size;
+
+      img {
+        height: 100%;
+        width: 100%;
+      }
+    }
+
+    > .details {
+      flex-grow: 1;
+      width: $person-card-width - $person-card-avatar-size;
+
+      > div {
+        line-height: 16px;
+        overflow: hidden;
+        text-overflow: ellipsis;
+        white-space: nowrap;
+      }
+
+      > :first-child {
+        color: $text-color;
+        font-size: 14px;
+      }
+
+      > :not(:first-child) {
+        color: $text-color-subtext;
+        font-size: 12px;
+      }
 
-    > .person-card-row-1 {
-      color: $text-color;
-      font-size: 14px;
+      > .secondary-details {
+        border-top: 1px solid #dae1e1;
+        margin-top: 20px;
+        padding-top: 8px;
+      }
     }
 
-    > .person-card-row-2,
-    > .person-card-row-3,
-    > .person-card-row-4 {
-      color: $text-color-subtext;
+    > .reports {
+      background-color: #dae1e1;
+      border-radius: 2px;
+      color: #434c50;
       font-size: 12px;
+      height: 18px;
+      line-height: 18px;
+      position: absolute;
+      right: 3px;
+      text-align: center;
+      top: 3px;
+      min-width: 25px;
     }
   }
 }

+ 27 - 2
src/main/angular/src/peoplesearch/person-card.component.ts

@@ -1,20 +1,38 @@
 import { Component } from '../component';
 import Person from '../models/person.model';
+import { IPeopleService } from '../services/people.service';
 
 
 @Component({
     bindings: {
-        person: '<'
+        directReports: '<',
+        person: '<',
+        size: '@'
     },
     stylesheetUrl: require('peoplesearch/person-card.component.scss'),
     templateUrl: require('peoplesearch/person-card.component.html')
 })
 export default class PersonCardComponent {
-    private person: Person;
     private data: string[];
+    private details: any[]; // For large style cards
+    private person: Person;
+    private size: string;
 
     static $inject = [];
     constructor() {
+    }
+
+    $onInit() {
+        this.details = [];
+    }
+
+    $onChanges() {
+        if (this.person) {
+            this.setDisplayData();
+        }
+    }
+
+    private setDisplayData() {
         // This data is only available on people search views
         if (this.person.givenName && this.person.sn) {
             this.data = [
@@ -28,5 +46,12 @@ export default class PersonCardComponent {
         else {
             this.data = this.person.displayNames;
         }
+
+        // This data is only available on details and orgchart views
+        if (this.person.detail) {
+            this.details = Object
+                .keys(this.person.detail)
+                .map((key: string) => { return this.person.detail[key]; });
+        }
     }
 }

+ 94 - 24
src/main/angular/src/services/people.data.json

@@ -707,7 +707,7 @@
     "mail": "tgutierreza@godaddy.com",
     "telephoneNumber": "(205) 653-6795",
     "title": "Engineer IV",
-    "managerId": 3,
+    "managerId": 9,
     "photoURL": "http://localhost:8080/pwm/public/resources/UserPhoto.png",
     "displayNames": [
       "Tina Gutierrez",
@@ -761,10 +761,10 @@
         "label": "Manager",
         "type": "person-link",
         "values": [
-          "William Carter"
+          "Jack Jackson"
         ],
         "typeMetaData": {
-          "userKey": 3
+          "userKey": 9
         }
       }
     }
@@ -777,7 +777,7 @@
     "mail": "jcoxb@stumbleupon.com",
     "telephoneNumber": "(816) 816-8474",
     "title": "Human Resources Manager",
-    "managerId": 3,
+    "managerId": 9,
     "photoURL": "http://localhost:8080/pwm/public/resources/UserPhoto.png",
     "displayNames": [
       "Jose Cox",
@@ -831,10 +831,10 @@
         "label": "Manager",
         "type": "person-link",
         "values": [
-          "William Carter"
+          "Jack Jackson"
         ],
         "typeMetaData": {
-          "userKey": 3
+          "userKey": 9
         }
       }
     }
@@ -917,7 +917,7 @@
     "mail": "jstevensd@ocn.ne.jp",
     "telephoneNumber": "(133) 596-4078",
     "title": "Automation Specialist I",
-    "managerId": 4,
+    "managerId": 14,
     "photoURL": "http://localhost:8080/pwm/public/resources/UserPhoto.png",
     "displayNames": [
       "Joe Stevens",
@@ -971,10 +971,10 @@
         "label": "Manager",
         "type": "person-link",
         "values": [
-          "Alan Snyder"
+          "Joe Stevens"
         ],
         "typeMetaData": {
-          "userKey": 4
+          "userKey": 14
         }
       }
     }
@@ -987,7 +987,7 @@
     "mail": "rgrante@europa.eu",
     "telephoneNumber": "(343) 776-3486",
     "title": "Safety Technician I",
-    "managerId": 4,
+    "managerId": 20,
     "photoURL": "http://localhost:8080/pwm/public/resources/UserPhoto.png",
     "displayNames": [
       "Randy Grant",
@@ -1041,10 +1041,10 @@
         "label": "Manager",
         "type": "person-link",
         "values": [
-          "Alan Snyder"
+          "Bruce Carroll"
         ],
         "typeMetaData": {
-          "userKey": 4
+          "userKey": 20
         }
       }
     }
@@ -1127,7 +1127,7 @@
     "mail": "cporterg@a8.net",
     "telephoneNumber": "(594) 905-7773",
     "title": "Data Coordiator",
-    "managerId": 9,
+    "managerId": 16,
     "photoURL": "http://localhost:8080/pwm/public/resources/UserPhoto.png",
     "displayNames": [
       "Cynthia Porter",
@@ -1181,10 +1181,10 @@
         "label": "Manager",
         "type": "person-link",
         "values": [
-          "Jack Jackson"
+          "Martin Mason"
         ],
         "typeMetaData": {
-          "userKey": 9
+          "userKey": 16
         }
       }
     }
@@ -1197,7 +1197,7 @@
     "mail": "nburnsh@wordpress.org",
     "telephoneNumber": "(444) 231-5492",
     "title": "Business Systems Development Analyst",
-    "managerId": 9,
+    "managerId": 17,
     "photoURL": "http://localhost:8080/pwm/public/resources/UserPhoto.png",
     "displayNames": [
       "Nancy Burns",
@@ -1251,10 +1251,10 @@
         "label": "Manager",
         "type": "person-link",
         "values": [
-          "Jack Jackson"
+          "Cynthia Porter"
         ],
         "typeMetaData": {
-          "userKey": 9
+          "userKey": 17
         }
       }
     }
@@ -1267,7 +1267,7 @@
     "mail": "jmontgomeryi@addtoany.com",
     "telephoneNumber": "(675) 799-8793",
     "title": "Structural Engineer",
-    "managerId": 9,
+    "managerId": 18,
     "photoURL": "http://localhost:8080/pwm/public/resources/UserPhoto.png",
     "displayNames": [
       "Jimmy Montgomery",
@@ -1321,10 +1321,10 @@
         "label": "Manager",
         "type": "person-link",
         "values": [
-          "Jack Jackson"
+          "Nancy Burns"
         ],
         "typeMetaData": {
-          "userKey": 9
+          "userKey": 18
         }
       }
     }
@@ -1337,7 +1337,7 @@
     "mail": "bcarrollj@paypal.com",
     "telephoneNumber": "(658) 289-4550",
     "title": "Desktop Support Technician",
-    "managerId": 9,
+    "managerId": 19,
     "photoURL": "http://localhost:8080/pwm/public/resources/UserPhoto.png",
     "displayNames": [
       "Bruce Carroll",
@@ -1391,10 +1391,80 @@
         "label": "Manager",
         "type": "person-link",
         "values": [
-          "Jack Jackson"
+          "Jimmy Montgomery"
         ],
         "typeMetaData": {
-          "userKey": 9
+          "userKey": 19
+        }
+      }
+    }
+  },
+  {
+    "userKey": 21,
+    "id": "6aeec76a-6a36-4825-965b-b9b5a009827a",
+    "givenName": "Orphan",
+    "sn": "User",
+    "mail": "orphan.user@gmail.com",
+    "telephoneNumber": "(454) 249-4440",
+    "title": "No Real Position",
+    "managerId": null,
+    "photoURL": "http://localhost:8080/pwm/public/resources/UserPhoto.png",
+    "displayNames": [
+      "Orphan User",
+      "orphan.user@gmail.com",
+      "No Real Position",
+      "(454) 249-4440"
+    ],
+    "detail": {
+      "sn": {
+        "name": "sn",
+        "label": "Last Name",
+        "type": "text",
+        "values": [
+          "User"
+        ]
+      },
+      "givenName": {
+        "name": "givenName",
+        "label": "First Name",
+        "type": "text",
+        "values": [
+          "Orphan"
+        ]
+      },
+      "telephoneNumber": {
+        "name": "telephoneNumber",
+        "label": "Phone",
+        "type": "tel",
+        "values": [
+          "(454) 249-4440"
+        ]
+      },
+      "mail": {
+        "name": "mail",
+        "label": "Email Address",
+        "type": "email",
+        "values": [
+          "orphan.user@gmail.com"
+        ]
+      },
+      "title": {
+        "name": "title",
+        "label": "Title",
+        "type": "text",
+        "values": [
+          "No Real Position"
+        ]
+      },
+      "manager": {
+        "name": "manager",
+        "label": "Manager",
+        "type": "person-link",
+        "values": [
+          "#N/A"
+        ],
+        "typeMetaData": {
+          "userKey": 0
         }
       }
     }

+ 12 - 0
src/main/angular/src/ux/app-bar.component.scss

@@ -0,0 +1,12 @@
+app-bar {
+  display: block;
+
+  > .app-bar-content {
+    display: flex;
+    flex-flow: row nowrap;
+
+    > [flex] {
+      flex: 1 1;
+    }
+  }
+}

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

@@ -0,0 +1,9 @@
+import { Component } from '../component';
+
+@Component({
+    stylesheetUrl: require('ux/app-bar.component.scss'),
+    template: `<div class="app-bar-content" ng-transclude></div>`,
+    transclude: true
+})
+export default class AppBarComponent {
+}

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

@@ -0,0 +1,22 @@
+$icon-button-size: 20px;
+$icon-button-bg-color: #808080;
+
+$icon-button-hover-bg-color: #f6f9f8;
+//$icon-button-hover-border-color: #dae1e1;
+$icon-button-hover-border-color: #0088ce;
+$icon-button-hover-color: #0088ce;
+
+icon-button {
+  background-color: $icon-button-bg-color;
+  border: 1px solid transparent;
+  display: block;
+  flex: 0 0 $icon-button-size;
+  height: $icon-button-size;
+  width: $icon-button-size;
+
+  &:hover {
+    background-color: $icon-button-hover-bg-color;
+    border-color: $icon-button-hover-border-color;
+    color: $icon-button-hover-color;
+  }
+}

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

@@ -0,0 +1,7 @@
+import { Component } from '../component';
+
+@Component({
+    stylesheetUrl: require('ux/icon-button.component.scss')
+})
+export default class IconButtonComponent {
+}

+ 11 - 0
src/main/angular/src/ux/ux.module.ts

@@ -0,0 +1,11 @@
+import { module } from 'angular';
+import AppBarComponent from './app-bar.component';
+import IconButtonComponent from './icon-button.component';
+
+var moduleName = 'peoplesearch.ux';
+
+module(moduleName, [ ])
+    .component('appBar', AppBarComponent)
+    .component('iconButton', IconButtonComponent);
+
+export default moduleName;

+ 2 - 1
src/main/angular/webpack.common.js

@@ -62,7 +62,8 @@ module.exports = {
     plugins: [
         new CopyWebpackPlugin([
             { from: 'vendor/angular-ui-router.js', to: 'vendor/' },
-            { from: 'node_modules/angular/angular.js', to: 'vendor/' }
+            { from: 'node_modules/angular/angular.js', to: 'vendor/' },
+            { from: 'images/', to: 'images/' }
         ]),
 
         new HtmlWebpackPlugin({