Browse Source

AutoCompleteComponent usability isses fixed. AutoCompleteComponent code cleaned up by moving template generation into template function. DI broken in production. TSLint rule for console.log/error.

Joe Hawkins 8 years ago
parent
commit
c28927ba99

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

@@ -22,12 +22,17 @@
 
 
 import * as angular from 'angular';
+import { IAugmentedJQuery, IAttributes} from 'angular';
+
+export interface IContentTemplateFunction {
+    ($element: IAugmentedJQuery, $attrs?: IAttributes): string;
+}
 
 export function Component(options: {
     bindings?: any,
     bindToController?: boolean,
     controllerAs?: string,
-    template?: string,
+    template?: (string | any[] | IContentTemplateFunction),
     templateUrl?: string,
     transclude?: boolean,
     stylesheetUrl?: string

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

@@ -58,5 +58,5 @@ module('app', [
     .factory('translationsLoader', TranslationsLoaderFactory);
 
 // Attach to the page document
-bootstrap(document, ['app']);
+bootstrap(document, ['app'], { strictDi: true });
 

+ 2 - 1
src/main/angular/src/peoplesearch/orgchart-search.component.html

@@ -1,6 +1,7 @@
 <mf-app-bar>
     <div class="page-content-title" translate="Title_Organization">Organization</div>
-    <mf-auto-complete query="$ctrl.query"
+    <mf-auto-complete search-text="$ctrl.query"
+                      on-search-text-change="$ctrl.onSearchTextChange(value)"
                       search="$ctrl.autoCompleteSearch(query)"
                       item-selected="$ctrl.onAutoCompleteItemSelected(person)"
                       item="person">

+ 7 - 3
src/main/angular/src/peoplesearch/orgchart-search.component.ts

@@ -77,8 +77,8 @@ export default class OrgChartSearchComponent {
                         self.person = data['person'];
                     });
                 })
-                .catch((result) => {
-                    console.log(result);
+                .catch(() => {
+                    // TODO: error handling
                 });
             });
     }
@@ -92,7 +92,11 @@ export default class OrgChartSearchComponent {
     }
 
     onAutoCompleteItemSelected(person: Person): void {
-        this.$state.go('orgchart.search', { personId: person.userKey });
+        this.$state.go('orgchart.search', { personId: person.userKey, query: null });
+    }
+
+    onSearchTextChange(value: string): void {
+        this.query = value;
     }
 
     private fetchOrgChartData(personId): IPromise<OrgChartData> {

+ 22 - 10
src/main/angular/src/peoplesearch/peoplesearch-base.component.ts

@@ -64,18 +64,24 @@ export default class PeopleSearchBaseComponent {
             this.query = queryParameter.trim();
         }
 
-        const self = this;
+        this.fetchData();
+    }
 
-        // Fetch data when query changes
-        this.$scope.$watch('$ctrl.query', (newValue: string, oldValue: string) => {
-            if (newValue === oldValue) {
-                return;
-            }
+    onSearchBoxKeyDown(event: KeyboardEvent): void {
+        switch (event.keyCode) {
+            case 27: // ESC
+                this.clearSearch();
+                break;
+        }
+    }
 
-            self.setSearchMessage(null);
-            self.fetchData();
-        });
+    onSearchTextChange(value: string): void {
+        if (value === this.query) {
+            return;
+        }
 
+        this.query = value;
+        this.setSearchMessage(null);
         this.fetchData();
     }
 
@@ -106,7 +112,7 @@ export default class PeopleSearchBaseComponent {
         const self = this;
 
         if (!this.query) {
-            self.searchResult = null;
+            this.clearSearch();
             return;
         }
 
@@ -132,6 +138,12 @@ export default class PeopleSearchBaseComponent {
             });
     }
 
+    private clearSearch(): void {
+        this.query = null;
+        this.searchResult = null;
+        this.clearSearchMessage();
+    }
+
     private clearSearchMessage(): void  {
         this.searchMessage = null;
     }

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

@@ -1,6 +1,9 @@
 <mf-app-bar>
     <div class="page-content-title" translate="Title_PeopleSearch">People Search</div>
-    <mf-search-bar search-text="$ctrl.query" auto-focus></mf-search-bar>
+    <mf-search-bar search-text="$ctrl.query"
+                   on-search-text-change="$ctrl.onSearchTextChange(value)"
+                   on-key-down="$ctrl.onSearchBoxKeyDown($event)"
+                   auto-focus></mf-search-bar>
     <span flex></span>
     <mf-icon-button
             icon="view-list"

+ 7 - 1
src/main/angular/src/peoplesearch/peoplesearch-cards.component.ts

@@ -39,7 +39,13 @@ export enum PeopleSearchCardsSize {
 })
 export default class PeopleSearchCardsComponent extends PeopleSearchBaseComponent {
     static $inject = [
-        '$element', '$scope', '$state', '$stateParams', '$translate', 'MfElementSizeService', 'PeopleService'
+        '$element',
+        '$scope',
+        '$state',
+        '$stateParams',
+        '$translate',
+        'MfElementSizeService',
+        'PeopleService'
     ];
     constructor(private $element: IAugmentedJQuery,
                 $scope: IScope,

+ 4 - 1
src/main/angular/src/peoplesearch/peoplesearch-table.component.html

@@ -1,6 +1,9 @@
 <mf-app-bar>
     <div class="page-content-title" translate="Title_PeopleSearch">People Search</div>
-    <mf-search-bar search-text="$ctrl.query" auto-focus></mf-search-bar>
+    <mf-search-bar search-text="$ctrl.query"
+                   on-search-text-change="$ctrl.onSearchTextChange(value)"
+                   on-key-down="$ctrl.onSearchBoxKeyDown($event)"
+                   auto-focus></mf-search-bar>
     <span flex></span>
     <mf-icon-button
             icon="view-tile"

+ 4 - 3
src/main/angular/src/peoplesearch/person-card.component.ts

@@ -66,9 +66,10 @@ export default class PersonCardComponent {
                 this.peopleService.getNumberOfDirectReports(this.person.userKey)
                     .then((numDirectReports) => {
                         this.person.numDirectReports = numDirectReports;
-                    }).catch((result) => {
-                    console.log(result);
-                });
+                    })
+                    .catch(() => {
+                        // TODO: error handling
+                    });
             }
         }
     }

+ 1 - 1
src/main/angular/src/services/people.service.ts

@@ -47,7 +47,7 @@ export default class PeopleService implements IPeopleService {
         return this.search(query, { 'includeDisplayName': true })
             .then((searchResult: SearchResult) => {
                 let people = searchResult.people;
-                console.log(people);
+
                 if (people && people.length > 10) {
                     return this.$q.resolve(people.slice(0, 10));
                 }

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

@@ -39,10 +39,6 @@ export default class AppBarComponent {
     constructor(private $element: IAugmentedJQuery, private elementSizeService: ElementSizeService) {
     }
 
-    $onDestroy(): void {
-        // TODO: remove $window click listener
-    }
-
     $onInit(): void {
         this.elementSizeService.watchWidth(this.$element, AppBarSize);
     }

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

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

+ 92 - 60
src/main/angular/src/ux/auto-complete.component.ts

@@ -22,93 +22,111 @@
 
 
 import { Component } from '../component';
-import { IAugmentedJQuery, ICompileService, IDocumentService, IPromise, IScope } from 'angular';
+import { IAttributes, IAugmentedJQuery, IDocumentService, IPromise, IScope } from 'angular';
 
 @Component({
     bindings: {
-        'search': '&',
+        'onSearchTextChange': '&',
         'itemSelected': '&',
         'item': '@',
         'itemText': '@',
-        'query': '='
+        'searchFunction': '&search',
+        'searchText': '<'
     },
-    templateUrl: require('ux/auto-complete.component.html'),
-    transclude: true,
+    template: [
+        '$element',
+        '$attrs',
+        ($element: IAugmentedJQuery, $attrs: IAttributes): string => {
+            // Remove content template from dom
+            const contentTemplate: IAugmentedJQuery = $element.find('content-template');
+            contentTemplate.detach();
+
+            return `
+                <mf-search-bar search-text="$ctrl.searchText"
+                               on-search-text-change="$ctrl.onSearchBarTextChange(value)"                           
+                               on-key-down="$ctrl.onSearchBarKeyDown($event)"
+                               ng-click="$ctrl.onSearchBarClick($event)"
+                               auto-focus></mf-search-bar>
+                <ul class="results" ng-if="$ctrl.show" ng-click="$event.stopPropagation()">
+                    <li ng-repeat="item in $ctrl.items"
+                       ng-click="$ctrl.selectItem(item)"
+                       ng-class="{ \'selected\': $index == $ctrl.selectedIndex }\">` +
+                contentTemplate.html().replace(new RegExp($attrs['item'], 'g'), 'item') +
+                    `</li>
+                    <li class="search-message" ng-if="$ctrl.show && $ctrl.searchText && !$ctrl.items.length">
+                        <span translate="Display_SearchResultsNone"></span>
+                    </li>
+                </ul>`;
+        }],
     stylesheetUrl: require('ux/auto-complete.component.scss')
 })
 export default class AutoCompleteComponent {
     item: string;
     items: any[];
     itemSelected: (item: any) => void;
-    query: string;
-    search: (query: any) => IPromise<any[]>;
+    onSearchTextChange: Function;
+    searchText: string;
+    searchFunction: (query: any) => IPromise<any[]>;
     searchMessage: string;
     selectedIndex: number;
     show: boolean;
 
-    static $inject = [ '$compile', '$document', '$element', '$scope' ];
-    constructor(private $compile: ICompileService,
-                private $document: IDocumentService,
+    static $inject = [ '$document', '$element', '$scope' ];
+    constructor(private $document: IDocumentService,
                 private $element: IAugmentedJQuery,
                 private $scope: IScope) {
     }
 
+    $onDestroy(): void {
+        this.$document.off('click', this.onDocumentClick.bind(this));
+    }
+
     $onInit(): void {
-        const self = this;
+        this.selectedIndex = -1;
 
-        this.$scope.$watch('$ctrl.query', () => {
-            self.search({ query: self.query })
-                .then((results: any[]) => {
-                    self.items = results;
-                    self.resetSelection();
-                    self.showAutoCompleteResults();
-                });
-        });
+        if (this.searchText) {
+            this.fetchAutoCompleteData(this.searchText);
+        }
 
-        this.selectedIndex = -1;
+        this.hideResults();
+    }
+
+    $postLink(): void {
+        var self = this;
 
         // Listen for clicks outside of the auto-complete component
-        this.$document.on('click', (event: Event) => {
-            if (self.show) {
-                self.hideAutoCompleteResults();
-                self.$scope.$apply();
-            }
-        });
+        // Implemented as a click event instead of a blur event, so the results list can be clicked
+        this.$document.on('click', this.onDocumentClick.bind(this));
     }
 
-    $postLink(): void {
-        // Remove content template from dom
-        const contentTemplate: IAugmentedJQuery = this.$element.find('content-template');
-        // noinspection TypeScriptUnresolvedFunction
-        contentTemplate.remove();
-
-        const autoCompleteHtml =
-            '<ul class="results" ng-if="$ctrl.show" ng-click="$event.stopPropagation()">' +
-            '   <li ng-repeat="item in $ctrl.items"' +
-            '       ng-click="$ctrl.selectItem(item)"' +
-            '       ng-class="{ \'selected\': $index == $ctrl.selectedIndex }\">' +
-            contentTemplate.html().replace(new RegExp(this.item, 'g'), 'item') +
-            '   </li>' +
-            '   <li class="search-message" ng-if="$ctrl.show && $ctrl.query && !$ctrl.items.length">' +
-            '       <span translate="Display_SearchResultsNone"></span>' +
-            '   </li>' +
-            '</ul>';
-        const compiledElement = this.$compile(autoCompleteHtml)(this.$scope);
-
-        this.$element.append(compiledElement);
+    onSearchBarClick(event: Event): void {
+        event.stopImmediatePropagation();
     }
 
     onSearchBarFocus(): void {
         if (this.hasItems()) {
-            this.showAutoCompleteResults();
+            this.showResults();
         }
     }
 
+    onSearchBarTextChange(value: string): void {
+        this.searchText = value;
+        this.fetchAutoCompleteData(value);
+        this.showResults();
+
+        this.onSearchTextChange({ value: value });
+    }
+
     onSearchBarKeyDown(event: KeyboardEvent): void {
         switch (event.keyCode) {
             case 40: // ArrowDown
-                this.selectNextItem();
-                event.preventDefault();
+                if (this.hasItems() && !this.show) {
+                    this.showResults();
+                }
+                else {
+                    this.selectNextItem();
+                    event.preventDefault();
+                }
                 break;
             case 38: // ArrowUp
                 this.selectPreviousItem();
@@ -116,10 +134,10 @@ export default class AutoCompleteComponent {
                 break;
             case 27: // Escape
                 if (!this.show || !this.hasItems()) {
-                    this.clearAutoCompleteResults();
+                    this.clearResults();
                 }
                 else {
-                    this.hideAutoCompleteResults();
+                    this.hideResults();
                 }
 
                 break;
@@ -130,7 +148,7 @@ export default class AutoCompleteComponent {
                 }
                 break;
             case 9: // Tab
-                if (!this.query) {
+                if (!this.searchText || !this.show) {
                     return;
                 }
 
@@ -147,19 +165,35 @@ export default class AutoCompleteComponent {
     }
 
     selectItem(item: any): void {
-        this.hideAutoCompleteResults();
+        this.clearResults();
 
         const data = {};
         data[this.item] = item;
         this.itemSelected(data);
     }
 
-    private clearAutoCompleteResults(): void {
+    private clearResults(): void {
         this.resetSelection();
-        this.query = null;
+        this.searchText = null;
         this.items = [];
     }
 
+    private onDocumentClick(): void {
+        if (this.show) {
+            this.hideResults();
+            this.$scope.$apply();
+        }
+    }
+
+    private fetchAutoCompleteData(value: string): void {
+        var self = this;
+        this.searchFunction({ query: value })
+            .then((results: any[]) => {
+                self.items = results;
+                self.resetSelection();
+            });
+    }
+
     private getSelectedItem(): any {
         return this.items[this.selectedIndex];
     }
@@ -168,7 +202,7 @@ export default class AutoCompleteComponent {
         return this.items && !!this.items.length;
     }
 
-    private hideAutoCompleteResults(): void  {
+    private hideResults(): void  {
         this.show = false;
     }
 
@@ -188,9 +222,7 @@ export default class AutoCompleteComponent {
         }
     }
 
-    private showAutoCompleteResults(): void  {
-        if (this.hasItems() || !/^\s*$/.test(this.query)) {
-            this.show = true;
-        }
+    private showResults(): void  {
+        this.show = true;
     }
 }

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

@@ -1,6 +1,8 @@
 <mf-icon class="search-icon" icon="magnify"></mf-icon>
 <input type="text"
-       ng-keydown="$ctrl.onInputKeyDown($event)"
+       ng-blur="$ctrl.onBlur({ $event: $event })"
+       ng-focus="$ctrl.onFocus({ $event: $event })"
+       ng-keydown="$ctrl.onKeyDown({ $event: $event })"
        ng-model="$ctrl.searchText"
        ng-attr-placeholder="{{ ('Placeholder_Search' | translate) }}"
        title="Search Box"

+ 17 - 10
src/main/angular/src/ux/search-bar.component.ts

@@ -22,13 +22,17 @@
 
 
 import { Component } from '../component';
-import { IAugmentedJQuery, ICompileService, IScope, ITimeoutService } from 'angular';
+import { IAugmentedJQuery, ICompileService, IScope } from 'angular';
 
 
 @Component({
     bindings: {
         autoFocus: '@',
-        searchText: '='
+        onSearchTextChange: '&',
+        onBlur: '&',
+        onFocus: '&',
+        onKeyDown: '&',
+        searchText: '<'
     },
     templateUrl: require('ux/search-bar.component.html'),
     stylesheetUrl: require('ux/search-bar.component.scss')
@@ -36,6 +40,7 @@ import { IAugmentedJQuery, ICompileService, IScope, ITimeoutService } from 'angu
 export default class SearchBarComponent {
     autoFocus: boolean;
     focused: boolean;
+    onSearchTextChange: Function;
     searchText: string;
 
     static $inject = [ '$compile', '$element', '$scope' ];
@@ -46,6 +51,16 @@ export default class SearchBarComponent {
 
     $onInit(): void {
         this.autoFocus = this.autoFocus !== undefined;
+
+        var self = this;
+
+        this.$scope.$watch('$ctrl.searchText', (newValue: string, oldValue: string) => {
+            if (newValue === oldValue) {
+                return;
+            }
+
+            self.onSearchTextChange({ value: newValue });
+        });
     }
 
     $postLink() {
@@ -66,12 +81,4 @@ export default class SearchBarComponent {
     focusInput() {
         this.$element.find('input')[0].focus();
     }
-
-    onInputKeyDown(event: KeyboardEvent): void {
-        switch (event.keyCode) {
-            case 27: // Escape
-                this.clearSearchText();
-                break;
-        }
-    }
 }

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

@@ -88,16 +88,16 @@ export default class TableDirectiveController {
         return this.items;
     }
 
-    getDisplayValue(item: any, valueExpression: string): any {
-        let value = this.getValue(item, valueExpression);
-
-        if (this.searchHighlight) {
-            return this
-                .$filter<(input: string, searchText: string) => string>('highlight')(value, this.searchHighlight);
-        }
-
-        return value;
-    }
+    // getDisplayValue(item: any, valueExpression: string): any {
+    //     let value = this.getValue(item, valueExpression);
+    //
+    //     if (this.searchHighlight) {
+    //         return this
+    //             .$filter<(input: string, searchText: string) => string>('highlight')(value, this.searchHighlight);
+    //     }
+    //
+    //     return value;
+    // }
 
     getValue(item: any, valueExpression: string): any {
         const locals: any = {};

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

@@ -31,7 +31,7 @@
     <tbody>
         <tr ng-repeat="item in table.getItems()" ng-click="table.clickItem(item, $event)">
             <td ng-repeat="column in table.getVisibleColumns()"
-                ng-bind-html="table.getDisplayValue(item, column.valueExpression)"></td>
+                ng-bind="table.getValue(item, column.valueExpression)"></td>
         </tr>
     </tbody>
 </table>

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

@@ -20,6 +20,7 @@
       120
     ],
     "no-bitwise": true,
+    "no-console": [true, "log", "error"],
     "no-duplicate-variable": true,
     "no-eval": true,
     "no-arg": true,