Browse Source

PeopleSearch advanced search (development mode) - initial commit

Joseph White 7 years ago
parent
commit
91e3efa135

+ 5 - 0
client/src/i18n/translations_en.json

@@ -1,8 +1,10 @@
 {
+  "Button_AddSearchAttribute": "Add Search Attribute",
   "Button_Attributes": "User Data",
   "Button_Cancel": "Cancel",
   "Button_ChangePassword": "Change Password",
   "Button_ClearResponses": "Clear Answers",
+  "Button_Close": "Close",
   "Button_CloseWindow": "Close Window",
   "Button_Confirm": "Confirm",
   "Button_Delete": "Delete",
@@ -12,6 +14,7 @@
   "Button_More": "More",
   "Button_OK": "OK",
   "Button_OTP": "OTP",
+  "Button_Remove": "Remove",
   "Button_Show": "Show",
   "Button_SMS": "SMS",
   "Button_Unlock": "Unlock",
@@ -31,6 +34,7 @@
   "Display_Random": "Random",
   "Display_SearchResultsExceeded": "Search results exceeded maximum search size",
   "Display_SearchResultsNone": "No results",
+  "Display_SelectAttribute": "Select attribute...",
   "Display_SetRandomPasswordPrompt": "Set a new random password for this user?",
   "Display_StrengthMeter": "Password Strength",
   "Display_TokenDestination": "Token Destination",
@@ -45,6 +49,7 @@
   "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.",
   "Placeholder_Search": "Search",
+  "Title_AdvancedSearch": "Advanced Search",
   "Title_ChangePassword": "Change Password",
   "Title_DirectReports": "Direct Report(s)",
   "Title_HelpDesk": "Help Desk",

+ 41 - 6
client/src/modules/peoplesearch/peoplesearch-base.component.ts

@@ -30,9 +30,21 @@ import { IPerson } from '../../models/person.model';
 import PromiseService from '../../services/promise.service';
 import SearchResult from '../../models/search-result.model';
 
-const SEARCH_TEXT_LOCAL_STORAGE_KEY = 'searchText';
-
 abstract class PeopleSearchBaseComponent {
+    advancedSearch = false;
+    advancedSearchTags = [
+        { id: 'userKey', label: 'User ID' },
+        { id: 'givenName', label: 'First Name' },
+        { id: 'sn', label: 'Last Name' },
+        { id: 'mail', label: 'Email' },
+        { id: 'telephoneNumber', label: 'Telephone Number' },
+        { id: 'title', label: 'Title' },
+        { id: 'workforceId', label: 'Workforce ID' },
+        { id: 'managerId', label: 'Manager User ID' },
+        { id: '_displayName', label: 'Display Name' },
+        { id: 'displayNames', label: 'Display Names' },
+        { id: 'detail', label: 'Detail' }
+    ];
     errorMessage: string;
     inputDebounce: number;
     orgChartEnabled: boolean;
@@ -40,6 +52,7 @@ abstract class PeopleSearchBaseComponent {
     searchMessage: string;
     searchResult: SearchResult;
     query: string;
+    queries = [{name: null, value: ''}];
     searchTextLocalStorageKey: string;
     searchViewLocalStorageKey: string;
 
@@ -76,15 +89,26 @@ abstract class PeopleSearchBaseComponent {
         this.$state.go(state, { query: this.query });
     }
 
+    private initiateSearch() {
+        this.clearSearchMessage();
+        this.clearErrorMessage();
+        this.fetchData();
+    }
+
     private onSearchTextChange(newValue: string, oldValue: string): void {
         if (newValue === oldValue) {
             return;
         }
 
         this.storeSearchText();
-        this.clearSearchMessage();
-        this.clearErrorMessage();
-        this.fetchData();
+        this.initiateSearch();
+    }
+
+    removeSearchTag(tagIndex: number): void {
+        this.queries.splice(tagIndex, 1);
+    }
+    addSearchTag(): void {
+        this.queries.push({name: null, value: ''});
     }
 
     selectPerson(person: IPerson): void {
@@ -161,7 +185,14 @@ abstract class PeopleSearchBaseComponent {
 
         const self = this;
 
-        let promise = this.peopleService.search(this.query);
+        let promise;
+
+        if (this.advancedSearch) {
+            promise = this.peopleService.advancedSearch(this.queries);
+        }
+        else {
+            promise = this.peopleService.search(this.query);
+        }
 
         this.pendingRequests.push(promise);
 
@@ -228,6 +259,10 @@ abstract class PeopleSearchBaseComponent {
         this.localStorageService.setItem(this.searchTextLocalStorageKey, this.query || '');
     }
 
+    toggleAdvancedSearch(): void {
+        this.advancedSearch = !this.advancedSearch;
+    }
+
     protected toggleView(state: string): void {
         this.storeSearchView(state);
         this.storeSearchText();

+ 44 - 5
client/src/modules/peoplesearch/peoplesearch-cards.component.html

@@ -21,28 +21,67 @@
   -->
 
 <div class="ias-header">
-    <h2 id="page-content-title" translate="Title_PeopleSearch">People Search</h2>
-    <ias-search-box id="input" ng-model="$ctrl.query" ng-model-options="{debounce: $ctrl.inputDebounce}"
+    <h2 id="page-content-title" ng-if="!$ctrl.advancedSearch" translate="Title_PeopleSearch">People Search</h2>
+    <h2 id="page-content-title" ng-if="$ctrl.advancedSearch">People Search Advanced Search</h2>
+    <ias-search-box id="input" ng-model="$ctrl.query"
+                    ng-if="!$ctrl.advancedSearch"
+                    ng-model-options="{debounce: $ctrl.inputDebounce}"
                     placeholder="{{'Placeholder_Search' | translate}}" auto-focus>
     </ias-search-box>
 
+    <ias-button id="advanced-search-icon" class="ias-icon-button" ng-click="$ctrl.toggleAdvancedSearch()"
+                ng-if="!$ctrl.advancedSearch"
+                ng-attr-title="{{ 'Title_AdvancedSearch' | translate }}">
+        <ias-icon class="ias-selected" icon="search_advanced"></ias-icon>
+    </ias-button>
+    <ias-button id="close-advanced-search-icon" class="ias-icon-button" ng-click="$ctrl.toggleAdvancedSearch()"
+                ng-if="$ctrl.advancedSearch" ng-attr-title="{{ 'Button_Close' | translate }}">
+        <ias-icon class="ias-selected" icon="close_thin"></ias-icon>
+    </ias-button>
+
     <span class="ias-fill"></span>
 
     <ias-button id="view-tile-icon" class="ias-icon-button ias-selected" ng-disabled="true"
                 ng-attr-title="{{ 'Title_PeopleSearchCard' | translate }}">
-        <ias-icon class="ias-selected" icon="view_tile_thin"></ias-icon>
+        <ias-icon icon="view_tile_thin"></ias-icon>
     </ias-button>
     <ias-button id="view-list-icon" class="ias-icon-button" ng-click="$ctrl.gotoTableView()"
                 ng-attr-title="{{ 'Title_PeopleSearchTable' | translate }}">
-        <ias-icon class="ias-selected" icon="view_list_thin"></ias-icon>
+        <ias-icon icon="view_list_thin"></ias-icon>
     </ias-button>
     <div class="icon-divider vertical" ng-if="$ctrl.orgChartEnabled"></div>
     <ias-button id="orgchart-icon" class="ias-icon-button" ng-click="$ctrl.gotoOrgchart()" ng-if="$ctrl.orgChartEnabled"
                 ng-attr-title="{{ 'Title_OrgChart' | translate }}">
-        <ias-icon class="ias-selected" icon="orgchart_thin"></ias-icon>
+        <ias-icon icon="orgchart_thin"></ias-icon>
     </ias-button>
 </div>
 
+<div ng-if="$ctrl.advancedSearch">
+    <div ng-repeat="query in $ctrl.queries">
+        <select ng-model="query.name">
+            <option value="" selected disabled>{{ 'Display_SelectAttribute' | translate }}</option>
+            <option ng-repeat="tag in $ctrl.advancedSearchTags" ng-attr-value="{{tag.id}}">{{tag.label}}</option>
+        </select>
+        <input ng-model="query.value" autocomplete="off">
+        <ias-button class="ias-icon-button" ng-click="$ctrl.removeSearchTag($index)" ng-if="$index > 0"
+                    ng-attr-title="{{ 'Button_Remove' | translate }}">
+            <ias-icon icon="close_thin"></ias-icon>
+        </ias-button>
+    </div>
+
+    <br>
+    <ias-button id="advanced-search-icon" class="ias-icon-button" ng-click="$ctrl.addSearchTag()"
+                ng-attr-title="{{ 'Button_AddSearchAttribute' | translate }}">
+        <ias-icon icon="new_thin"></ias-icon>
+    </ias-button>
+
+    <br><br>
+    <ias-button id="advanced-search-button" ng-click="$ctrl.initiateSearch()"
+                ng-bind="'Placeholder_Search' | translate">
+    </ias-button>
+
+</div>
+
 <div class="search-info-container">
     <div class="search-info" ng-class="{'loading': !$ctrl.getMessage()}"
          ng-if="$ctrl.loading || $ctrl.searchMessage || $ctrl.errorMessage"

+ 4 - 0
client/src/modules/peoplesearch/peoplesearch-cards.component.scss

@@ -57,6 +57,10 @@ people-search-cards {
     }
   }
 
+  .ias-search {
+    margin-right: 10px;
+  }
+
   > .people-search-component-content {
     flex: 1 1;
     overflow: auto;

+ 54 - 1
client/src/services/people.service.dev.ts

@@ -23,7 +23,7 @@
 
 import { IPromise, IQService, ITimeoutService } from 'angular';
 import { IPerson } from '../models/person.model';
-import { IPeopleService } from './people.service';
+import {IPeopleService, IQuery} from './people.service';
 import IOrgChartData from '../models/orgchart-data.model';
 import SearchResult from '../models/search-result.model';
 
@@ -62,6 +62,33 @@ export default class PeopleService implements IPeopleService {
         }, this);
     }
 
+    advancedSearch(queries: IQuery[]): IPromise<SearchResult> {
+        let self = this;
+
+        let deferred = this.$q.defer();
+        let deferredAbort = this.$q.defer();
+
+        let timeoutPromise = this.$timeout(() => {
+
+            let people = this.getAdvancedSearchResults(queries);
+            const sizeExceeded = (people.length > MAX_RESULTS);
+            if (sizeExceeded) {
+                people = people.slice(MAX_RESULTS);
+            }
+
+            deferred.resolve(new SearchResult({sizeExceeded: sizeExceeded, searchResults: people}));
+        }, SIMULATED_RESPONSE_TIME * 6);
+
+        // To simulate an abortable promise, edit SIMULATED_RESPONSE_TIME
+        deferred.promise['_httpTimeout'] = deferredAbort;
+        deferredAbort.promise.then(() => {
+            self.$timeout.cancel(timeoutPromise);
+            deferred.resolve();
+        });
+
+        return deferred.promise as IPromise<SearchResult>;
+    }
+
     autoComplete(query: string): IPromise<IPerson[]> {
         return this.search(query)
             .then((searchResult: SearchResult) => {
@@ -207,4 +234,30 @@ export default class PeopleService implements IPeopleService {
 
         return null;
     }
+
+    private getAdvancedSearchResults(queries: IQuery[]): IPerson[] {
+        let people = queries.length ? this.people : [];
+
+        queries.forEach((query: IQuery) => {
+            people = people.filter((person: IPerson) => {
+                if (!query.value) {
+                    return false;
+                }
+
+                let property = person[query.name];
+
+                if (!property) {
+                    return false;
+                }
+
+                if (typeof property === 'object' || typeof property === 'number') {
+                    property = JSON.stringify(property);
+                }
+
+                return property.toLowerCase().indexOf(query.value.toLowerCase()) >= 0;
+            });
+        });
+
+        return people;
+    }
 }

+ 11 - 0
client/src/services/people.service.ts

@@ -28,7 +28,14 @@ import IOrgChartData from '../models/orgchart-data.model';
 import SearchResult from '../models/search-result.model';
 import {IPeopleSearchConfigService} from './peoplesearch-config.service';
 
+export interface IQuery {
+ name: string;
+ value: string;
+}
+
 export interface IPeopleService {
+    advancedSearch(queries: IQuery[]): IPromise<SearchResult>;
+
     autoComplete(query: string): IPromise<IPerson[]>;
 
     getDirectReports(personId: string): IPromise<IPerson[]>;
@@ -63,6 +70,10 @@ export default class PeopleService implements IPeopleService {
         }
     }
 
+    advancedSearch(queries: IQuery[]): IPromise<SearchResult> {
+        return null;
+    }
+
     autoComplete(query: string): IPromise<IPerson[]> {
         return this.search(query, {'includeDisplayName': true})
             .then((searchResult: SearchResult) => {