initial commit of tag search ui code

This commit is contained in:
John Mitchell 2016-03-28 11:39:37 -04:00
parent bf09d38c5b
commit 6311e8b8b3
12 changed files with 649 additions and 38 deletions

View File

@ -51,6 +51,7 @@ import activityStream from './activity-stream/main';
import standardOut from './standard-out/main';
import lookUpHelper from './lookup/main';
import JobTemplates from './job-templates/main';
import search from './search/main';
import {ScheduleEditController} from './controllers/Schedules';
import {ProjectsList, ProjectsAdd, ProjectsEdit} from './controllers/Projects';
import OrganizationsList from './organizations/list/organizations-list.controller';
@ -109,6 +110,7 @@ var tower = angular.module('Tower', [
standardOut.name,
access.name,
JobTemplates.name,
search.name,
'templates',
'Utilities',
'OrganizationFormDefinition',

View File

@ -392,9 +392,6 @@ export default
scope[iterator + 'HoldInput' + modifier] = true;
if ($('#search-widget-container' + modifier) &&
list.fields[scope[iterator + 'SearchField' + modifier]] && !list.fields[scope[iterator + 'SearchField' + modifier]].searchObject) {
// if the search widget exists and its value is not an object, add its parameters to the query
if (scope[iterator + 'SearchValue' + modifier]) {
// if user typed a value in the input box, show the reset link
scope[iterator + 'ShowStartBtn' + modifier] = false;

View File

@ -30,6 +30,7 @@ export default
icon: "icon-job-{{ project.statusIcon }}",
columnClass: "List-staticColumn--smallStatus",
nosort: true,
searchLabel: 'Status',
searchType: 'select',
searchOptions: [], //set in the controller
excludeModal: true

View File

@ -0,0 +1,31 @@
export default [function() {
// given the list, return the fields that need searching
this.getList = function(list) {
return JSON.stringify(Object
.keys(list.fields)
.filter(function(i) {
return (list.fields[i]
.searchable !== false);
}).map(function(i) {
return {[i]: list.fields[i]};
}).reduce(function (acc, i) {
var key = Object.keys(i);
acc[key] = i[key];
return acc;
}));
};
// given the list config object, return the basepath
this.getEndpoint = function(list) {
return list.basePath || list.name;
};
// inject the directive with the list and endpoint
this.inject = function(list, endpoint) {
return "<tag-search list='" + list +
"' endpoint='" + endpoint +
"'></tag-search>";
};
return this;
}];

View File

@ -0,0 +1,9 @@
import tagSearchDirective from './tagSearch.directive';
import tagSearchService from './tagSearch.service';
import getSearchHtml from './getSearchHtml.service';
export default
angular.module('search', [])
.directive('tagSearch', tagSearchDirective)
.factory('tagSearchService', tagSearchService)
.factory('getSearchHtml', getSearchHtml);

View File

@ -0,0 +1,239 @@
@import "../shared/branding/colors.default.less";
.TagSearch {
margin-bottom: 10px;
}
.TagSearch-bar {
display: flex;
padding: 0;
font-size: 12px;
height: 35px;
align-items: stretch;
margin-bottom: 10px;
}
.TagSearch-bar i {
font-size: 16px;
color: @default-icon;
}
.TagSearch-typeDropdown {
color: @default-interface-txt;
flex: initial;
border: 1px solid @default-second-border;
border-top-left-radius: 5px;
border-bottom-left-radius: 5px;
padding: 0px 10px;
display: flex;
white-space: nowrap;
align-items: center;
max-height: 400px;
overflow-y: scroll;
width: 100px;
cursor: pointer;
}
.TagSearch-typeDropdown.is-open {
border-bottom-left-radius: 0;
}
.TagSearch-typeDropdownName {
width: 66px;
text-overflow: ellipsis;
display: block;
overflow: hidden;
}
.TagSearch-selectDownIcon {
margin-left: 10px;
}
.TagSearch-dropdownContainer {
position: absolute;
left: 15px;
top: 34px;
font-size: 14px;
border-radius: 5px;
border: 1px solid @default-second-border;
background: white;
padding: 5px 0;
border-top-left-radius: 0px;
border-top-right-radius: 0px;
z-index: 50000;
max-height: 200px;
overflow-y: scroll;
box-shadow: 2px 2px 2px 0px rgba(0, 0, 0, 0.1);
}
.TagSearch-dropdownContainer--searchTypes {
min-width: 96px;
}
.TagSearch-dropdownContainer--typeOptions {
right: 15px;
left: initial;
width: ~"calc(100% - 123px)";
}
.TagSearch-dropdownItem {
padding: 5px 10px;
cursor: pointer;
}
.TagSearch-dropdownItem:hover {
background-color: @default-tertiary-bg;
}
.TagSearch-dropdownItem.is-selected {
background-color: @default-button-hov;
}
.TagSearch-searchTermContainer {
flex: initial;
width: ~"calc(100% - 70px)";
border: 1px solid @default-second-border;
border-left: 0px;
border-top-right-radius: 5px;
border-bottom-right-radius: 5px;
display: flex;
background-color: @default-secondary-bg;
}
.TagSearch-searchTermContainer.is-open {
border-bottom-right-radius: 0;
}
.TagSearch-searchTermContainer input {
flex: 1 0 auto;
margin: 0 10px;
border: none;
font-size: 14px;
}
.TagSearch-searchTermContainer input:focus,
.TagSearch-searchTermContainer input:active {
outline: 0;
}
.TagSearch-searchTermSelect {
padding: 0px 10px !important;
display: flex;
align-items: center;
width: 100%;
}
.TagSearch-searchTermSelectPlaceholder {
color: @default-icon !important;
text-transform: uppercase;
font-size: 14px;
flex: 1 0 auto;
}
.TagSearch-searchTermContainer input:placeholder-shown {
color: @default-icon !important;
text-transform: uppercase;
}
.TagSearch-searchTermSelect {
padding: 10px;
background-color: @default-bg;
border-top-right-radius: 5px;
border-bottom-right-radius: 5px;
cursor: pointer;
}
.TagSearch-searchButton {
flex: initial;
margin-left: auto;
padding: 8px 10px;
border-left: 1px solid @default-second-border;
background-color: @default-bg;
cursor: pointer;
border-top-right-radius: 5px;
border-bottom-right-radius: 5px;
}
.TagSearch-searchButton:hover {
background-color: @default-tertiary-bg;
}
.TagSearch-flexContainer {
display: flex;
width: 100%;
flex-wrap: wrap;
}
.TagSearch-tagContainer {
display: flex;
max-width: 100%;
margin-bottom: 10px;
}
.TagSearch-tag {
border-radius: 5px;
padding: 2px 10px;
margin: 4px 0px;
border: 1px solid @default-second-border;
font-size: 12px;
color: @default-interface-txt;
text-transform: uppercase;
background-color: @default-bg;
margin-right: 5px;
max-width: 100%;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
.TagSearch-tag--deletable {
margin-right: 0px;
border-top-right-radius: 0px;
border-bottom-right-radius: 0px;
border-right: 0;
max-wdith: ~"calc(100% - 23px)";
}
.TagSearch-deleteContainer {
border: 1px solid @default-second-border;
border-top-right-radius: 5px;
border-bottom-right-radius: 5px;
padding: 0 5px;
margin: 4px 0px;
margin-right: 5px;
align-items: center;
display: flex;
cursor: pointer;
}
.TagSearch-tagDelete {
font-size: 13px;
color: @default-icon;
}
.TagSearch-name {
flex: initial;
max-width: 100%;
}
.TagSearch-tag--deletable > .TagSearch-name {
max-width: ~"calc(100% - 23px)";
}
.TagSearch-deleteContainer:hover, {
border-color: @default-err;
background-color: @default-err;
}
.TagSearch-deleteContainer:hover > .TagSearch-tagDelete {
color: @default-bg;
}
// TODO: Actually Remove the old list search widgets from the partials
.List-searchWidget {
display: none !important;
}
.List-searchRow {
display: none !important;
}

View File

@ -0,0 +1,105 @@
export default ['$scope', 'Refresh', 'tagSearchService',
function($scope, Refresh, tagSearchService) {
// JSONify passed field elements that can be searched
$scope.list = JSON.parse($scope.list);
// Hotfix: GetBasePath to work with inventories
$scope.$watch("endpoint", function(val) {
if (val === 'inventories') {
$scope.endpoint = 'inventory';
}
});
// Grab options for the left-dropdown of the searchbar
tagSearchService.getSearchTypes($scope.list, $scope.endpoint)
.then(function(searchTypes) {
$scope.searchTypes = searchTypes;
// currently selected option of the left-dropdown
$scope.currentSearchType = $scope.searchTypes[0];
});
// shows/hide the search type dropdown
$scope.toggleTypeDropdown = function() {
$scope.showTypeDropdown = !$scope.showTypeDropdown;
};
// sets the search type dropdown and hides it
$scope.setSearchType = function(type) {
$scope.currentSearchType = type;
$scope.showTypeDropdown = false;
};
// if the current search type uses a list instead
// of a text input, this show hides that list
$scope.toggleCurrentSearchDropdown = function() {
$scope
.showCurrentSearchDropdown = !$scope
.showCurrentSearchDropdown;
};
$scope.updateSearch = function(tags) {
var iterator = $scope.$parent.list.iterator;
var pageSize = $scope
.$parent[iterator + "_page_size"];
var set = $scope.$parent.list.name;
var listScope = $scope.$parent;
var url = tagSearchService
.updateFilteredUrl($scope.endpoint, tags, pageSize);
$scope.$parent[iterator + "_active_search"] = true;
Refresh({
scope: listScope,
set: set,
iterator: iterator,
url: url
});
$scope.currentSearchFilters = tags;
};
// triggers a refilter of the list with the newTag
$scope.addTag = function(type) {
var newTag = tagSearchService
.getTag($scope.currentSearchType,
$scope.newSearchTag,
type);
// reset the search bar
$scope.resetSearchBar();
// make a clone of the currentSearchFilters
// array and push the newTag to this array
var tags = tagSearchService
.getCurrentTags($scope
.currentSearchFilters);
if (!tagSearchService.isDuplicate(tags, newTag)) {
tags.push(newTag);
$scope.updateSearch(tags);
}
};
// triggers a refilter of the list without the oldTag
$scope.deleteTag = function(oldTag) {
// make a clone of the currentSearchFilters
// array and remove oldTag from the array
var tags = tagSearchService
.getCurrentTags($scope
.currentSearchFilters)
.filter(function(tag) {
return tag.url !== oldTag.url;
});
$scope.updateSearch(tags);
};
// make sure all stateful UI triggers are reset
$scope.resetSearchBar = function() {
delete $scope.currentSearchSelectedOption;
$scope.newSearchTag = null;
$scope.showTypeDropdown = false;
$scope.showCurrentSearchDropdown = false;
};
}];

View File

@ -0,0 +1,30 @@
import tagSearchController from './tagSearch.controller';
/* jshint unused: vars */
export default
['templateUrl',
function(templateUrl) {
return {
restrict: 'E',
scope: {
list: '@',
endpoint: '@',
iterator: '@'
},
controller: tagSearchController,
templateUrl: templateUrl('search/tagSearch'),
link: function(scope, element, attrs) {
// make the enter button work as if clicking the
// search icon
element
.find('.TagSearch-searchTermInput')
.bind('keypress', function (e) {
var code = e.keyCode || e.which;
if (code === 13) {
scope.addTag();
}
});
}
};
}
];

View File

@ -0,0 +1,78 @@
<div class="TagSearch row">
<div class="col-lg-4 col-md-8 col-sm-12 col-xs-12">
<div class="TagSearch-bar">
<div class="TagSearch-typeDropdown"
ng-click="toggleTypeDropdown()"
ng-class="{'is-open': showTypeDropdown}">
<span class="TagSearch-typeDropdownName">
{{ currentSearchType.label || "Foo bar"}}
</span>
<i class="TagSearch-selectDownIcon fa fa-angle-down"></i>
</div>
<div aw-click-off="showTypeDropdown" class="TagSearch-dropdownContainer
TagSearch-dropdownContainer--searchTypes"
ng-show="showTypeDropdown">
<div class="TagSearch-dropdownItem"
ng-repeat="type in searchTypes track by $index"
ng-class="{'is-selected': (currentSearchType.value === type.value)}"
ng-click="setSearchType(type)">
{{ type.label }}
</div>
</div>
<div class="TagSearch-searchTermContainer"
ng-class="{'is-open': showCurrentSearchDropdown}">
<input class="TagSearch-searchTermInput"
ng-show="currentSearchType.type === 'text'"
ng-model="newSearchTag"
placeholder="Search">
<div class="TagSearch-searchButton"
ng-disabled="!newSearchTag"
ng-click="addTag()"
ng-show="currentSearchType.type === 'text'">
<i class="fa fa-search"></i>
</div>
<div class="TagSearch-searchTermSelect"
ng-show="currentSearchType.type === 'select'"
ng-click="toggleCurrentSearchDropdown()">
<div class="TagSearch-searchTermSelectPlaceholder">
Filter
</div>
<i class="TagSearch-selectDownIcon
fa fa-angle-down">
</i>
</div>
<div aw-click-off="showCurrentSearchDropdown"
class="TagSearch-dropdownContainer
TagSearch-dropdownContainer--typeOptions"
ng-show="showCurrentSearchDropdown &&
currentSearchType.type === 'select'">
<div class="TagSearch-dropdownItem"
ng-repeat="type in currentSearchType.typeOptions track by $index"
ng-click="addTag(type)">
{{ type.label }}
</div>
</div>
</div>
</div>
</div>
<div class="col-lg-8 col-md-12 col-sm-12 col-xs-12">
<div class="TagSearch-tagSection">
<div class="TagSearch-flexContainer">
<div class="TagSearch-tagContainer"
ng-repeat="tag in currentSearchFilters track by $index">
<div class="TagSearch-tag TagSearch-tag--deletable"
<span class="TagSearch-name">
{{ tag.name }}
</span>
</div>
<div class="TagSearch-deleteContainer"
ng-click="deleteTag(tag)">
<i class="fa fa-times TagSearch-tagDelete"></i>
</div>
</div>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,148 @@
export default ['Rest', '$q', 'GetBasePath', function(Rest, $q, GetBasePath) {
var that = this;
// parse the field config object to return
// one of the searchTypes (for the left dropdown)
this.buildType = function (field, key, id) {
// build the value (key)
var value;
if (typeof(field.key) === String) {
value = field.key;
} else {
value = key;
}
// build the label
var label = field.searchLabel || field.label;
// build the search type
var type, typeOptions;
if (field.searchType === 'select') {
type = 'select';
typeOptions = [];
} else if (field.searchType === 'boolean') {
type = 'select';
typeOptions = [{label: "Yes", value: true},
{label: "No", value: false}];
} else {
type = 'text';
}
// return the built option
if (type === 'select') {
return {
id: id,
value: value,
label: label,
type: type,
typeOptions: typeOptions
};
} else {
return {
id: id,
value: value,
label: label,
type: type
};
}
};
// given the fields that are searchable,
// return searchTypes in the format the view can use
this.getSearchTypes = function(list, basePath) {
var defer = $q.defer();
var options = Object
.keys(list)
.map(function(key, id) {
return that.buildType(list[key], key, id);
});
var needsRequest, passThrough;
// splits off options that need a request from
// those that don't
var partitionedOptions = _.partition(options, function(opt) {
return (opt.typeOptions && !opt.typeOptions
.length) ? true : false;
});
needsRequest = partitionedOptions[0];
passThrough = partitionedOptions[1];
var joinOptions = function() {
return _.sortBy(_
.flatten([needsRequest, passThrough]), function(opt) {
return opt.id;
});
};
if (needsRequest.length) {
// make the options request to reutrn the typeOptions
Rest.setUrl(GetBasePath(basePath));
Rest.options()
.success(function (data) {
var options = data.actions.GET;
needsRequest = needsRequest
.map(function (option) {
option.typeOptions = options[option
.value]
.choices
.map(function(i) {
return {
value: i[0],
label: i[1]
};
});
return option;
});
defer.resolve(joinOptions());
});
} else {
defer.resolve(joinOptions());
}
return defer.promise;
};
// returns the url with filter params
this.updateFilteredUrl = function(basePath, tags, pageSize) {
return GetBasePath(basePath) + "?" +
(tags || []).map(function (t) {
return t.url;
}).join("&") + "&page_size=" + pageSize;
};
// given the field and input filters, create the tag object
this.getTag = function(field, textVal, selectVal) {
var tag = _.clone(field);
if (tag.type === "text") {
tag.url = tag.value + "__icontains=" + textVal;
tag.name = textVal;
} else {
tag.url = tag.value + "=" + selectVal.value;
tag.name = selectVal.label;
}
return tag;
};
// returns true if the newTag is already in the list of tags
this.isDuplicate = function(tags, newTag) {
return (tags
.filter(function(tag) {
return (tag.url === newTag.url);
}).length > 0);
};
// returns an array of tags (or empty array if there are none)
// .slice(0) is used so the currentTags variable is not directly mutated
this.getCurrentTags = function(currentTags) {
if (currentTags && currentTags.length) {
return currentTags.slice(0);
}
return [];
};
return this;
}];

View File

@ -4,7 +4,7 @@
@default-icon: #B7B7B7;
@default-icon-hov: #D7D7D7; // also selected button
@default-border: #E8E8E8;
@default-second-border: #E1E1E1;
@default-second-border: #D7D7D7;
@default-bg: #FFFFFF; // also selected btn txt
@default-secondary-bg: #FCFCFC; // page/input field bg, just adds depth
@default-tertiary-bg: #FAFAFA; // hover bg, alt-list

View File

@ -98,9 +98,9 @@
import {templateUrl} from '../../shared/template-url/template-url.factory';
export default ['$location', '$compile', '$rootScope', 'SearchWidget', 'PaginateWidget', 'Attr', 'Icon',
export default ['$location', '$compile', '$rootScope', 'SearchWidget', 'PaginateWidget', 'Attr', 'Icon', 'getSearchHtml',
'Column', 'DropDown', 'NavigationLink', 'SelectIcon',
function ($location, $compile, $rootScope, SearchWidget, PaginateWidget, Attr, Icon, Column, DropDown, NavigationLink,
function ($location, $compile, $rootScope, SearchWidget, PaginateWidget, Attr, Icon, getSearchHtml, Column, DropDown, NavigationLink,
SelectIcon) {
return {
@ -362,38 +362,9 @@ export default ['$location', '$compile', '$rootScope', 'SearchWidget', 'Paginate
html += (list.emptyListText) ? list.emptyListText : "PLEASE ADD ITEMS TO THIS LIST";
html += "</div>";
if (options.showSearch=== undefined || options.showSearch === true) {
// Only show the search bar if we are loading results or if we have at least 1 base result
html += "<div class=\"row List-searchRow\" ng-show=\"" + list.iterator + "Loading == true || " + list.iterator + "_active_search == true || (" + list.iterator + "Loading == false && " + list.iterator + "_active_search == false && " + list.iterator + "_total_rows > 0)\">\n";
if (options.searchSize) {
html += SearchWidget({
iterator: list.iterator,
template: list,
mini: true,
size: options.searchSize,
searchWidgets: list.searchWidgets
});
} else if (options.mode === 'summary') {
html += SearchWidget({
iterator: list.iterator,
template: list,
mini: true,
size: 'col-lg-6'
});
} else if (options.mode === 'lookup' || options.id !== undefined) {
html += SearchWidget({
iterator: list.iterator,
template: list,
mini: true,
size: 'col-lg-8'
});
} else {
html += SearchWidget({
iterator: list.iterator,
template: list,
mini: true
});
}
html += "</div><!-- row -->\n";
html += getSearchHtml
.inject(getSearchHtml.getList(list),
getSearchHtml.getEndpoint(list));
// Message for when a search returns no results. This should only get shown after a search is executed with no results.
html += "<div class=\"row\" ng-show=\"" + list.iterator + "Loading == false && " + list.iterator + "_active_search == true && " + list.name + ".length == 0\">\n";