瀏覽代碼

Added persistence to the advanced searches for people search and help desk.

jalbr74 6 年之前
父節點
當前提交
fe9de1df2e

+ 41 - 38
client/src/modules/helpdesk/helpdesk-search-base.component.ts

@@ -30,6 +30,7 @@ import PromiseService from '../../services/promise.service';
 import {IHelpDeskService} from '../../services/helpdesk.service';
 import IPwmService from '../../services/pwm.service';
 import {IAdvancedSearchConfig, IAdvancedSearchQuery, IAttributeMetadata} from '../../services/base-config.service';
+import CommonSearchService from '../../services/common-search.service';
 
 
 let verificationsDialogTemplateUrl = require('./verifications-dialog.template.html');
@@ -64,36 +65,45 @@ export default abstract class HelpDeskSearchBaseComponent {
                 protected IasDialogService: any,
                 protected localStorageService: LocalStorageService,
                 protected promiseService: PromiseService,
-                protected pwmService: IPwmService) {
+                protected pwmService: IPwmService,
+                protected commonSearchService: CommonSearchService) {
         this.searchTextLocalStorageKey = this.localStorageService.keys.HELPDESK_SEARCH_TEXT;
         this.searchViewLocalStorageKey = this.localStorageService.keys.HELPDESK_SEARCH_VIEW;
 
         this.inputDebounce = this.pwmService.ajaxTypingWait;
-
-        $scope.$watch('$ctrl.query', (newValue: string, oldValue: string) => {
-            this.onSearchTextChange(newValue, oldValue);
-        });
     }
 
-    protected initialize(): void {
-        this.query = this.getSearchText();
-
-        this.configService.verificationsEnabled().then((verificationsEnabled: boolean) => {
-            this.verificationsEnabled = verificationsEnabled;
-        });
-
-        this.configService.advancedSearchConfig().then((advancedSearchConfig: IAdvancedSearchConfig) => {
-            this.advancedSearchEnabled = advancedSearchConfig.enabled;
-            this.advancedSearchMaxRows = advancedSearchConfig.maxRows;
-
-            for (let advancedSearchTag of advancedSearchConfig.attributes) {
-                this.advancedSearchTags[advancedSearchTag.attribute] = advancedSearchTag;
+    protected initialize(): IPromise<void> {
+        return this.$q.all(
+            [
+                this.configService.verificationsEnabled().then((verificationsEnabled: boolean) => {
+                    this.verificationsEnabled = verificationsEnabled;
+                }),
+                this.configService.advancedSearchConfig().then((advancedSearchConfig: IAdvancedSearchConfig) => {
+                    this.advancedSearchEnabled = advancedSearchConfig.enabled;
+                    this.advancedSearchMaxRows = advancedSearchConfig.maxRows;
+
+                    for (let advancedSearchTag of advancedSearchConfig.attributes) {
+                        this.advancedSearchTags[advancedSearchTag.attribute] = advancedSearchTag;
+                    }
+                })
+            ]
+        ).then(result => {
+            this.query = this.getSearchText();
+            this.advancedSearch = this.commonSearchService.isHdAdvancedSearchActive();
+            this.queries = this.commonSearchService.getHdAdvSearchQueries();
+            if (this.queries.length === 0) {
+                this.addSearchTag();
             }
-        });
 
-        // Once <ias-search-box> from ng-ias allows the autofocus attribute, we can remove this code
-        this.$timeout(() => {
-            document.getElementsByTagName('input')[0].focus();
+            // Once <ias-search-box> from ng-ias allows the autofocus attribute, we can remove this code
+            this.$timeout(() => {
+                document.getElementsByTagName('input')[0].focus();
+            });
+
+            this.$scope.$watch('$ctrl.query', (newValue: string, oldValue: string) => {
+                this.onSearchTextChange(newValue, oldValue);
+            });
         });
     }
 
@@ -182,7 +192,7 @@ export default abstract class HelpDeskSearchBaseComponent {
     }
 
     private gotoState(state: string): void {
-        this.$state.go(state, { query: this.query });
+        this.$state.go(state);
     }
 
     private initiateSearch() {
@@ -248,22 +258,26 @@ export default abstract class HelpDeskSearchBaseComponent {
         // Make sure we set the default value if the type is select
         const attributeMetadata: IAttributeMetadata = this.advancedSearchTags[query.key];
         if (attributeMetadata.type == 'select') {
-            query.value = this.getDefaultValue(attributeMetadata);
+            query.value = this.commonSearchService.getDefaultValue(attributeMetadata);
         }
 
+        this.commonSearchService.setHdAdvSearchQueries(this.queries);
         this.initiateSearch();
     }
 
     private onAdvancedSearchAttributeValueChanged() {
+        this.commonSearchService.setHdAdvSearchQueries(this.queries);
         this.initiateSearch();
     }
 
     private onAdvancedSearchValueChanged() {
+        this.commonSearchService.setHdAdvSearchQueries(this.queries);
         this.initiateSearch();
     }
 
     removeSearchTag(tagIndex: number): void {
         this.queries.splice(tagIndex, 1);
+        this.commonSearchService.setHdAdvSearchQueries(this.queries);
 
         if (this.queries.length > 0) {
             this.initiateSearch();
@@ -271,6 +285,7 @@ export default abstract class HelpDeskSearchBaseComponent {
         else {
             this.clearSearch();
             this.advancedSearch = false;
+            this.commonSearchService.setHdAdvancedSearchActive(this.advancedSearch);
         }
     }
 
@@ -280,25 +295,12 @@ export default abstract class HelpDeskSearchBaseComponent {
 
         const query: IAdvancedSearchQuery = {
             key: attributeMetaData.attribute,
-            value: this.getDefaultValue(attributeMetaData),
+            value: this.commonSearchService.getDefaultValue(attributeMetaData),
         };
 
         this.queries.push(query);
     }
 
-    private getDefaultValue(attributeMetaData: IAttributeMetadata) {
-        if (attributeMetaData) {
-            if (attributeMetaData.type === 'select') {
-                const keys: string[] = Object.keys(attributeMetaData.options);
-                if (keys && keys.length > 0) {
-                    return keys[0];
-                }
-            }
-        }
-
-        return '';
-    }
-
     protected selectPerson(person: IPerson): void {
         this.IasDialogService
             .open({
@@ -327,6 +329,7 @@ export default abstract class HelpDeskSearchBaseComponent {
         this.clearSearch();
         this.addSearchTag();
         this.advancedSearch = true;
+        this.commonSearchService.setHdAdvancedSearchActive(this.advancedSearch);
     }
 
     protected toggleView(state: string): void {

+ 9 - 5
client/src/modules/helpdesk/helpdesk-search-cards.component.ts

@@ -31,6 +31,7 @@ import {IPerson} from '../../models/person.model';
 import PromiseService from '../../services/promise.service';
 import {IHelpDeskService} from '../../services/helpdesk.service';
 import IPwmService from '../../services/pwm.service';
+import CommonSearchService from '../../services/common-search.service';
 
 @Component({
     stylesheetUrl: require('./helpdesk-search.component.scss'),
@@ -49,7 +50,8 @@ export default class HelpDeskSearchCardsComponent extends HelpDeskSearchBaseComp
         'IasDialogService',
         'LocalStorageService',
         'PromiseService',
-        'PwmService'
+        'PwmService',
+        'CommonSearchService'
     ];
     constructor($q: IQService,
                 $scope: IScope,
@@ -62,14 +64,16 @@ export default class HelpDeskSearchCardsComponent extends HelpDeskSearchBaseComp
                 IasDialogService: any,
                 localStorageService: LocalStorageService,
                 promiseService: PromiseService,
-                pwmService: IPwmService) {
+                pwmService: IPwmService,
+                commonSearchService: CommonSearchService) {
         super($q, $scope, $state, $stateParams, $timeout, $translate, configService, helpDeskService, IasDialogService,
-            localStorageService, promiseService, pwmService);
+            localStorageService, promiseService, pwmService, commonSearchService);
     }
 
     $onInit() {
-        this.initialize();
-        this.fetchData();
+        this.initialize().then(() => {
+            this.fetchData();
+        });
 
         this.configService.photosEnabled().then((photosEnabled: boolean) => {
             this.photosEnabled = photosEnabled;

+ 9 - 5
client/src/modules/helpdesk/helpdesk-search-table.component.ts

@@ -30,6 +30,7 @@ import LocalStorageService from '../../services/local-storage.service';
 import PromiseService from '../../services/promise.service';
 import {IHelpDeskService} from '../../services/helpdesk.service';
 import IPwmService from '../../services/pwm.service';
+import CommonSearchService from '../../services/common-search.service';
 
 @Component({
     stylesheetUrl: require('./helpdesk-search.component.scss'),
@@ -50,7 +51,8 @@ export default class HelpDeskSearchTableComponent extends HelpDeskSearchBaseComp
         'IasDialogService',
         'LocalStorageService',
         'PromiseService',
-        'PwmService'
+        'PwmService',
+        'CommonSearchService'
     ];
     constructor($q: IQService,
                 $scope: IScope,
@@ -63,14 +65,16 @@ export default class HelpDeskSearchTableComponent extends HelpDeskSearchBaseComp
                 IasDialogService: any,
                 localStorageService: LocalStorageService,
                 promiseService: PromiseService,
-                pwmService: IPwmService) {
+                pwmService: IPwmService,
+                commonSearchService: CommonSearchService) {
         super($q, $scope, $state, $stateParams, $timeout, $translate, configService, helpDeskService, IasDialogService,
-              localStorageService, promiseService, pwmService);
+              localStorageService, promiseService, pwmService, commonSearchService);
     }
 
     $onInit() {
-        this.initialize();
-        this.fetchData();
+        this.initialize().then(() => {
+            this.fetchData();
+        });
 
         // The table columns are dynamic and configured via a service
         this.configService.getColumnConfig().then(

+ 3 - 1
client/src/modules/helpdesk/helpdesk.module.ts

@@ -38,6 +38,7 @@ import AutogenChangePasswordController from '../../components/changepassword/aut
 import RandomChangePasswordController from '../../components/changepassword/random-change-password.controller';
 import SuccessChangePasswordController from '../../components/changepassword/success-change-password.controller';
 import TypeChangePasswordController from '../../components/changepassword/type-change-password.controller';
+import CommonSearchService from '../../services/common-search.service';
 
 require('../peoplesearch/peoplesearch.scss');
 
@@ -61,6 +62,7 @@ module(moduleName, [
     .filter('dateFilter', DateFilter)
     .service('ObjectService', ObjectService)
     .service('PromiseService', PromiseService)
-    .service('LocalStorageService', LocalStorageService);
+    .service('LocalStorageService', LocalStorageService)
+    .service('CommonSearchService', CommonSearchService);
 
 export default moduleName;

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

@@ -30,6 +30,7 @@ import { IPerson } from '../../models/person.model';
 import PromiseService from '../../services/promise.service';
 import SearchResult from '../../models/search-result.model';
 import {IAdvancedSearchConfig, IAdvancedSearchQuery, IAttributeMetadata} from '../../services/base-config.service';
+import CommonSearchService from '../../services/common-search.service';
 
 abstract class PeopleSearchBaseComponent {
     advancedSearch = false;
@@ -57,15 +58,12 @@ abstract class PeopleSearchBaseComponent {
                 protected localStorageService: LocalStorageService,
                 protected peopleService: IPeopleService,
                 protected promiseService: PromiseService,
-                protected pwmService: IPwmService) {
+                protected pwmService: IPwmService,
+                protected commonSearchService: CommonSearchService) {
         this.searchTextLocalStorageKey = this.localStorageService.keys.SEARCH_TEXT;
         this.searchViewLocalStorageKey = this.localStorageService.keys.SEARCH_VIEW;
 
         this.inputDebounce = this.pwmService.ajaxTypingWait;
-
-        $scope.$watch('$ctrl.query', (newValue: string, oldValue: string) => {
-            this.onSearchTextChange(newValue, oldValue);
-        });
     }
 
     getMessage(): string {
@@ -77,7 +75,7 @@ abstract class PeopleSearchBaseComponent {
     }
 
     private gotoState(state: string): void {
-        this.$state.go(state, { query: this.query });
+        this.$state.go(state);
     }
 
     private initiateSearch() {
@@ -90,17 +88,20 @@ abstract class PeopleSearchBaseComponent {
         // Make sure we set the default value if the type is select
         const attributeMetadata: IAttributeMetadata = this.advancedSearchTags[query.key];
         if (attributeMetadata.type == 'select') {
-            query.value = this.getDefaultValue(attributeMetadata);
+            query.value = this.commonSearchService.getDefaultValue(attributeMetadata);
         }
 
+        this.commonSearchService.setPsAdvSearchQueries(this.queries);
         this.initiateSearch();
     }
 
     private onAdvancedSearchAttributeValueChanged() {
+        this.commonSearchService.setPsAdvSearchQueries(this.queries);
         this.initiateSearch();
     }
 
     private onAdvancedSearchValueChanged() {
+        this.commonSearchService.setPsAdvSearchQueries(this.queries);
         this.initiateSearch();
     }
 
@@ -115,6 +116,7 @@ abstract class PeopleSearchBaseComponent {
 
     removeSearchTag(tagIndex: number): void {
         this.queries.splice(tagIndex, 1);
+        this.commonSearchService.setPsAdvSearchQueries(this.queries);
 
         if (this.queries.length > 0) {
             this.initiateSearch();
@@ -122,6 +124,7 @@ abstract class PeopleSearchBaseComponent {
         else {
             this.clearSearch();
             this.advancedSearch = false;
+            this.commonSearchService.setPsAdvancedSearchActive(this.advancedSearch);
         }
     }
 
@@ -131,25 +134,12 @@ abstract class PeopleSearchBaseComponent {
 
         const query: IAdvancedSearchQuery = {
             key: attributeMetaData.attribute,
-            value: this.getDefaultValue(attributeMetaData),
+            value: this.commonSearchService.getDefaultValue(attributeMetaData),
         };
 
         this.queries.push(query);
     }
 
-    private getDefaultValue(attributeMetaData: IAttributeMetadata) {
-        if (attributeMetaData) {
-            if (attributeMetaData.type === 'select') {
-                const keys: string[] = Object.keys(attributeMetaData.options);
-                if (keys && keys.length > 0) {
-                    return keys[0];
-                }
-            }
-        }
-
-        return '';
-    }
-
     selectPerson(person: IPerson): void {
         this.$state.go('.details', { personId: person.userKey, query: this.query });
     }
@@ -272,26 +262,38 @@ abstract class PeopleSearchBaseComponent {
             });
     }
 
-    protected initialize(): void {
-        // Determine whether org-chart should appear
-        this.configService.orgChartEnabled().then((orgChartEnabled: boolean) => {
-            this.orgChartEnabled = orgChartEnabled;
-        });
-
-        this.configService.advancedSearchConfig().then((advancedSearchConfig: IAdvancedSearchConfig) => {
-            this.advancedSearchEnabled = advancedSearchConfig.enabled;
-            this.advancedSearchMaxRows = advancedSearchConfig.maxRows;
-
-            for (let advancedSearchTag of advancedSearchConfig.attributes) {
-                this.advancedSearchTags[advancedSearchTag.attribute] = advancedSearchTag;
+    protected initialize(): IPromise<void> {
+        return this.$q.all(
+            [
+                // Determine whether org-chart should appear
+                this.configService.orgChartEnabled().then((orgChartEnabled: boolean) => {
+                    this.orgChartEnabled = orgChartEnabled;
+                }),
+                this.configService.advancedSearchConfig().then((advancedSearchConfig: IAdvancedSearchConfig) => {
+                    this.advancedSearchEnabled = advancedSearchConfig.enabled;
+                    this.advancedSearchMaxRows = advancedSearchConfig.maxRows;
+
+                    for (let advancedSearchTag of advancedSearchConfig.attributes) {
+                        this.advancedSearchTags[advancedSearchTag.attribute] = advancedSearchTag;
+                    }
+                })
+            ]
+        ).then(result => {
+            this.query = this.getSearchText();
+            this.advancedSearch = this.commonSearchService.isPsAdvancedSearchActive();
+            this.queries = this.commonSearchService.getPsAdvSearchQueries();
+            if (this.queries.length === 0) {
+                this.addSearchTag();
             }
-        });
 
-        this.query = this.getSearchText();
+            // Once <ias-search-box> from ng-ias allows the autofocus attribute, we can remove this code
+            this.$timeout(() => {
+                document.getElementsByTagName('input')[0].focus();
+            });
 
-        // Once <ias-search-box> from ng-ias allows the autofocus attribute, we can remove this code
-        this.$timeout(() => {
-            document.getElementsByTagName('input')[0].focus();
+            this.$scope.$watch('$ctrl.query', (newValue: string, oldValue: string) => {
+                this.onSearchTextChange(newValue, oldValue);
+            });
         });
     }
 
@@ -316,6 +318,7 @@ abstract class PeopleSearchBaseComponent {
         this.clearSearch();
         this.addSearchTag();
         this.advancedSearch = true;
+        this.commonSearchService.setPsAdvancedSearchActive(this.advancedSearch);
     }
 
     protected toggleView(state: string): void {

+ 10 - 5
client/src/modules/peoplesearch/peoplesearch-cards.component.ts

@@ -32,6 +32,7 @@ import PeopleSearchBaseComponent from './peoplesearch-base.component';
 import { IPerson } from '../../models/person.model';
 import PromiseService from '../../services/promise.service';
 import SearchResult from '../../models/search-result.model';
+import CommonSearchService from '../../services/common-search.service';
 
 export enum PeopleSearchCardsSize {
     Small = 0,
@@ -59,7 +60,8 @@ export default class PeopleSearchCardsComponent extends PeopleSearchBaseComponen
         'MfElementSizeService',
         'PeopleService',
         'PromiseService',
-        'PwmService'
+        'PwmService',
+        'CommonSearchService'
     ];
     constructor(private $element: IAugmentedJQuery,
                 $q: IQService,
@@ -73,7 +75,8 @@ export default class PeopleSearchCardsComponent extends PeopleSearchBaseComponen
                 private elementSizeService: ElementSizeService,
                 peopleService: IPeopleService,
                 promiseService: PromiseService,
-                pwmService: IPwmService) {
+                pwmService: IPwmService,
+                commonSearchService: CommonSearchService) {
         super($q,
             $scope,
             $state,
@@ -84,7 +87,8 @@ export default class PeopleSearchCardsComponent extends PeopleSearchBaseComponen
             localStorageService,
             peopleService,
             promiseService,
-            pwmService);
+            pwmService,
+            commonSearchService);
     }
 
     $onDestroy(): void {
@@ -92,8 +96,9 @@ export default class PeopleSearchCardsComponent extends PeopleSearchBaseComponen
     }
 
     $onInit(): void {
-        this.initialize();
-        this.fetchData();
+        this.initialize().then(() => {
+            this.fetchData();
+        });
 
         this.configService.photosEnabled().then((photosEnabled: boolean) => {
             this.photosEnabled = photosEnabled;

+ 10 - 5
client/src/modules/peoplesearch/peoplesearch-table.component.ts

@@ -30,6 +30,7 @@ import LocalStorageService from '../../services/local-storage.service';
 import PeopleSearchBaseComponent from './peoplesearch-base.component';
 import PromiseService from '../../services/promise.service';
 import SearchResult from '../../models/search-result.model';
+import CommonSearchService from '../../services/common-search.service';
 
 @Component({
     stylesheetUrl: require('./peoplesearch-table.component.scss'),
@@ -49,7 +50,8 @@ export default class PeopleSearchTableComponent extends PeopleSearchBaseComponen
         'LocalStorageService',
         'PeopleService',
         'PromiseService',
-        'PwmService'
+        'PwmService',
+        'CommonSearchService'
     ];
     constructor($q: IQService,
                 $scope: IScope,
@@ -61,7 +63,8 @@ export default class PeopleSearchTableComponent extends PeopleSearchBaseComponen
                 localStorageService: LocalStorageService,
                 peopleService: IPeopleService,
                 promiseService: PromiseService,
-                pwmService: IPwmService) {
+                pwmService: IPwmService,
+                commonSearchService: CommonSearchService) {
         super($q,
             $scope,
             $state,
@@ -72,12 +75,14 @@ export default class PeopleSearchTableComponent extends PeopleSearchBaseComponen
             localStorageService,
             peopleService,
             promiseService,
-            pwmService);
+            pwmService,
+            commonSearchService);
     }
 
     $onInit(): void {
-        this.initialize();
-        this.fetchData();
+        this.initialize().then(() => {
+            this.fetchData();
+        });
 
         let self = this;
 

+ 3 - 1
client/src/modules/peoplesearch/peoplesearch.module.ts

@@ -34,6 +34,7 @@ import PersonDetailsDialogComponent from './person-details-dialog.component';
 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');
 
@@ -53,6 +54,7 @@ module(moduleName, [
     .component('peopleSearchCards', PeopleSearchCardsComponent as IComponentOptions)
     .component('personDetailsDialogComponent', PersonDetailsDialogComponent as IComponentOptions)
     .service('PromiseService', PromiseService)
-    .service('LocalStorageService', LocalStorageService);
+    .service('LocalStorageService', LocalStorageService)
+    .service('CommonSearchService', CommonSearchService);
 
 export default moduleName;

+ 128 - 0
client/src/services/common-search.service.test.ts

@@ -0,0 +1,128 @@
+/*
+ * 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 {IHttpService, ILogService, IQService, IWindowService, module} from 'angular';
+import LocalStorageService from './local-storage.service';
+import CommonSearchService from './common-search.service';
+import {anyNumber, anyString, deepEqual, instance, mock, strictEqual, verify, when} from 'ts-mockito';
+import {IAdvancedSearchQuery} from './base-config.service';
+
+describe('In common-search.service.test.ts', () => {
+    beforeEach(() => {
+        module('app', []);
+    });
+
+    // Define some angular objects we'll grab from angular-mocks
+    let $log: ILogService;
+
+    beforeEach(inject((_$http_, _$log_, _$q_, _$window_) => {
+        $log = _$log_;
+        $log.info('This is an info message');
+    }));
+
+    it('Pulls search queries from local storage, or empty array if undefined or bad data', (done: DoneFn) => {
+        let mockLocalStorageService = mock(LocalStorageService);
+        when(mockLocalStorageService.getItem('undefinedScenario')).thenReturn(undefined);
+        when(mockLocalStorageService.getItem('bogusScenario')).thenReturn('bogus');
+        when(mockLocalStorageService.getItem('notArrayScenario')).thenReturn('{"key":"foo","value":"bar"}');
+        when(mockLocalStorageService.getItem('goodScenario')).thenReturn(JSON.stringify([
+            {
+                key: 'foo',
+                value: 'bar'
+            }
+        ]));
+
+        const commonSearchService = new CommonSearchService(instance(mockLocalStorageService));
+
+        expect(commonSearchService.getAdvSearchQueries('undefinedScenario')).toEqual([]);
+        expect(commonSearchService.getAdvSearchQueries('bogusScenario')).toEqual([]);
+        expect(commonSearchService.getAdvSearchQueries('notArrayScenario')).toEqual([]);
+        expect(commonSearchService.getAdvSearchQueries('goodScenario')).toContain({
+            key: 'foo',
+            value: 'bar'
+        });
+
+        done();
+    });
+
+    it('Stores search queries into local storage, or does nothing if bad data', (done: DoneFn) => {
+        let mockLocalStorageService = mock(LocalStorageService);
+
+        const queries: IAdvancedSearchQuery[] = [
+            {key: 'foo', value: 'one'},
+            {key: 'bar', value: 'two'},
+            {key: 'baz', value: 'three'}
+        ];
+
+        const commonSearchService = new CommonSearchService(instance(mockLocalStorageService));
+
+        commonSearchService.setAdvSearchQueries('nullData', null);
+        commonSearchService.setAdvSearchQueries('undefinedData', undefined);
+        commonSearchService.setAdvSearchQueries('emptyArray', []);
+        commonSearchService.setAdvSearchQueries('lotsOfData', queries);
+
+        verify(mockLocalStorageService.removeItem('nullData')).called();
+        verify(mockLocalStorageService.removeItem('undefinedData')).called();
+        verify(mockLocalStorageService.setItem('emptyArray', '[]')).called();
+        verify(mockLocalStorageService.setItem('emptyArray', anyString())).called();
+
+        done();
+    });
+
+    it('Pulls advanced search active state from local storage, or false if undefined or bad data', (done: DoneFn) => {
+        let mockLocalStorageService = mock(LocalStorageService);
+        when(mockLocalStorageService.getItem('undefinedScenario')).thenReturn(undefined);
+        when(mockLocalStorageService.getItem('bogusScenario')).thenReturn('bogus');
+        when(mockLocalStorageService.getItem('invalidScenario')).thenReturn('{}');
+        when(mockLocalStorageService.getItem('falseScenario')).thenReturn(JSON.stringify(false));
+        when(mockLocalStorageService.getItem('trueScenario')).thenReturn(JSON.stringify(true));
+
+        const commonSearchService = new CommonSearchService(instance(mockLocalStorageService));
+
+        expect(commonSearchService.isAdvSearchActive('undefinedScenario')).toEqual(false);
+        expect(commonSearchService.isAdvSearchActive('bogusScenario')).toEqual(false);
+        expect(commonSearchService.isAdvSearchActive('invalidScenario')).toEqual(false);
+        expect(commonSearchService.isAdvSearchActive('falseScenario')).toEqual(false);
+        expect(commonSearchService.isAdvSearchActive('trueScenario')).toEqual(true);
+
+        done();
+    });
+
+    it('Stores the advanced search active state to local storage', (done: DoneFn) => {
+        let mockLocalStorageService = mock(LocalStorageService);
+
+        const commonSearchService = new CommonSearchService(instance(mockLocalStorageService));
+
+        commonSearchService.setAdvSearchActive('nullData', null);
+        commonSearchService.setAdvSearchActive('undefinedData', undefined);
+        commonSearchService.setAdvSearchActive('trueScenario', true);
+        commonSearchService.setAdvSearchActive('falseScenario', false);
+
+        verify(mockLocalStorageService.removeItem('nullData')).called();
+        verify(mockLocalStorageService.removeItem('undefinedData')).called();
+        verify(mockLocalStorageService.setItem('trueScenario', 'true')).called();
+        verify(mockLocalStorageService.setItem('falseScenario', 'false')).called();
+
+        done();
+    });
+});

+ 132 - 0
client/src/services/common-search.service.ts

@@ -0,0 +1,132 @@
+/*
+ * 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 LocalStorageService from './local-storage.service';
+import {IAdvancedSearchQuery, IAttributeMetadata} from './base-config.service';
+
+const PS_ADV_SEARCH_ACTIVE = 'psAdvancedSearchActive';
+const PS_ADV_SEARCH_QUERIES = 'psAdvancedSearchQueries';
+const HD_ADV_SEARCH_ACTIVE = 'hdAdvancedSearchActive';
+const HD_ADV_SEARCH_QUERIES = 'hdAdvancedSearchQueries';
+
+export default class CommonSearchService {
+    static $inject = ['LocalStorageService'];
+    constructor(private localStorageService: LocalStorageService) {
+    }
+
+    getDefaultValue(attributeMetaData: IAttributeMetadata) {
+        if (attributeMetaData) {
+            if (attributeMetaData.type === 'select') {
+                const keys: string[] = Object.keys(attributeMetaData.options);
+                if (keys && keys.length > 0) {
+                    return keys[0];
+                }
+            }
+        }
+
+        return '';
+    }
+
+    isPsAdvancedSearchActive(): boolean {
+        return this.isAdvSearchActive(PS_ADV_SEARCH_ACTIVE);
+    }
+
+    setPsAdvancedSearchActive(active: boolean): void {
+        this.setAdvSearchActive(PS_ADV_SEARCH_ACTIVE, active);
+    }
+
+    getPsAdvSearchQueries(): IAdvancedSearchQuery[] {
+        return this.getAdvSearchQueries(PS_ADV_SEARCH_QUERIES);
+    }
+
+    setPsAdvSearchQueries(queries: IAdvancedSearchQuery[]) {
+        this.setAdvSearchQueries(PS_ADV_SEARCH_QUERIES, queries);
+    }
+
+    isHdAdvancedSearchActive(): boolean {
+        return this.isAdvSearchActive(HD_ADV_SEARCH_ACTIVE);
+    }
+
+    setHdAdvancedSearchActive(active: boolean): void {
+        this.setAdvSearchActive(HD_ADV_SEARCH_ACTIVE, active);
+    }
+
+    getHdAdvSearchQueries(): IAdvancedSearchQuery[] {
+        return this.getAdvSearchQueries(HD_ADV_SEARCH_QUERIES);
+    }
+
+    setHdAdvSearchQueries(queries: IAdvancedSearchQuery[]) {
+        this.setAdvSearchQueries(HD_ADV_SEARCH_QUERIES, queries);
+    }
+
+    isAdvSearchActive(storageName: string): boolean {
+        if (storageName) {
+            const storageValue = this.localStorageService.getItem(storageName);
+            if (storageValue) {
+                return (storageValue === 'true');
+            }
+        }
+
+        return false;
+    }
+
+    setAdvSearchActive(storageName: string, active: boolean): void {
+        if (storageName) {
+            // Make sure active is a boolean first
+            if (typeof(active) === typeof(true)) {
+                this.localStorageService.setItem(storageName, JSON.stringify(active));
+            } else {
+                // If we were given undefine or null data, then just remove the named item from local storage
+                this.localStorageService.removeItem(storageName);
+            }
+        }
+    }
+
+    getAdvSearchQueries(storageName: string): IAdvancedSearchQuery[] {
+        if (storageName) {
+            const storageValue = this.localStorageService.getItem(storageName);
+            if (storageValue) {
+                try {
+                    const parsedValue = JSON.parse(storageValue);
+                    if (Array.isArray(parsedValue)) {
+                        return parsedValue;
+                    }
+                } catch (error) {
+                    // Unparseable, an empty array will be returned below
+                }
+            }
+        }
+
+        return [];
+    }
+
+    setAdvSearchQueries(storageName: string, queries: IAdvancedSearchQuery[]) {
+        if (storageName) {
+            if (queries) {
+                this.localStorageService.setItem(storageName, JSON.stringify(queries));
+            } else {
+                // If we were given undefine or null data, then just remove the named item from local storage
+                this.localStorageService.removeItem(storageName);
+            }
+        }
+    }
+}

+ 1 - 1
client/test/karma-test-suite.ts

@@ -32,6 +32,6 @@ import 'angular-mocks';
 // var appContext = (require as any).context('../src', true, /\.test\.ts/);
 
 // To run a specific test, change the following regular expression and use this:
-var appContext = (require as any).context('../src', true, /orgchart.component.test.ts/);
+var appContext = (require as any).context('../src', true, /common-search.service.test.ts/);
 
 appContext.keys().forEach(appContext);