mirror of
https://github.com/ZwareBear/awx.git
synced 2026-04-26 09:51:48 -05:00
Boolean / Smart Search (#3631)
* Part 1: building new search components Directives: smart-search, column-sort, paginate Service: QuerySet Model: DjangoSearchModel * Part 2: Implementing new search components, de-implementing old search components Remove old code: * tagSearch directive * old pagination strategy * old column sorting strategy * lookup Add new directives to list/form generator: * smart-search, * paginate * column-sort Connect $state + dataset resolution * upgrade ui-router lib to v1.0.0-beta3 * Custom $urlMatcherFactory.type - queryset * Render lists, forms, related, lookups in named views * Provide html templates in list/form/lookup/related state definitions * Provide dataset through resolve block in state definitions Update utilities * isEmpty filter * use async validation strategy in awlookup directive * Part 3: State implementations (might split into per-module commits) * Support optional state definition flag: squashSearchUrl. *_search params are only URI-encoded if squashSearchUrl is falsey. * * Fix list badge counts * Clear search input after search term(s) applied * Chain of multiple search terms in one submission * Hook up activity stream * Hook up portal mode * Fix pagination range calculations * Hook up organization sub-list views * Hook up listDefinition.search defaults * Fix ng-disabled conditions reflecting RBAC access on form fields * Fix actively-editing indicator in generated lists * form generator - fix undefined span, remove dead event listeners * wrap hosts/groups lists in a panel, fix groups list error * Smart search directive: clear all search tags * Search tags - ‘Clear All’ text - 12px Search key - remove top padding/margin Search key - reverse bolding of relationship fields / label, add commas Search tags - remove padding-bottom Lookup modal - “X” close button styled incorrectly Lookup modal - List title not rendered Lookup modal - 20px margin between buttons * Portal Mode Fix default column-sort on jobs list Hide column-oort on job status column Apply custom search bar sizes * stateDefinition.factory Return ES6 Promise instead of $q promise. $q cannot be safely provided during module.config() phase Some generated state trees (inventory / inventoryManage) need to be reduced to one promise. Side-step issues caused by ui-router de-registering ALL registered states that match placeholder state name/url pattern. e.g. inventories.lazyLoad() would de-register inventoryManage states if a page refresh occured @ /#/inventories/** * Combine generated state trees: inventories + inventoryManage Hook up inventory sync schedule list/form add /form edit views * Hook up system job schedule list/add/edit states * Fix breadcrumb of generated states in /setup view Fix typo in scheduler search prefix * Remove old search system deritus from list definitions * Fix breadcrumb definitions in states registered in app.js config block * Transclude list action buttons in generated form lists * Lookup Modal passes acceptance criterea: Modal cancel/exit - don’t update form field’s ng-model Modal save - do update form field's ng-model Transclude generated list contents into <lookup-modal> directive Lookup modal test spec * Fix typo in merge conflict resolution * Disable failing unit tests pending revision * Integrate smart-search architechture into add-permissions modal * use a semicolon delimiter instead of comma to avoid collision with django __in comparator * Hook up Dashboard > Hosts states, update Dashboard Inventory/Project counts with new search filters * Misc bug splat Add 20px spacing around root ui-view Fix missing closing div in related views Remove dupe line in smart-search controller * Remove defunct LookupHelper code * Rebuild inventories list status tooltips on updates to dataset Code cleanup - remove defunct modules Remove LookupHelper / LookupInit code Remove pre-RBAC permissions module * Add mising stateTree / basePath properties to form definitions * Resolve i18n conflicts in list and form generator Freeze dependencies * Integrate sockets * Final bug splat: fix jobs > job details and jobs > scheduled routing fix mis-resolved merge conflicts swap console.info for $log.debug
This commit is contained in:
131
awx/ui/tests/spec/lookup/lookup-modal.directive-test.js
Normal file
131
awx/ui/tests/spec/lookup/lookup-modal.directive-test.js
Normal file
@@ -0,0 +1,131 @@
|
||||
'use strict';
|
||||
|
||||
xdescribe('Directive: lookupModal', () => {
|
||||
|
||||
let dom, element, listHtml, listDefinition, Dataset,
|
||||
lookupTemplate, paginateTemplate, searchTemplate, columnSortTemplate,
|
||||
$scope, $parent, $compile, $state;
|
||||
|
||||
// mock dependency chains
|
||||
// (shared requires RestServices requires Authorization etc)
|
||||
beforeEach(angular.mock.module('login'));
|
||||
beforeEach(angular.mock.module('shared'));
|
||||
|
||||
beforeEach(angular.mock.module('LookupModalModule', ($provide) => {
|
||||
$provide.value('smartSearch', angular.noop);
|
||||
$provide.value('columnSort', angular.noop);
|
||||
$provide.value('paginate', angular.noop);
|
||||
$state = jasmine.createSpyObj('$state', ['go']);
|
||||
}));
|
||||
|
||||
beforeEach(angular.mock.inject(($templateCache, _$rootScope_, _$compile_, _generateList_) => {
|
||||
listDefinition = {
|
||||
name: 'mocks',
|
||||
iterator: 'mock',
|
||||
fields: {
|
||||
name: {}
|
||||
}
|
||||
};
|
||||
|
||||
listHtml = _generateList_.build({
|
||||
mode: 'lookup',
|
||||
list: listDefinition,
|
||||
input_type: 'radio'
|
||||
});
|
||||
|
||||
Dataset = {
|
||||
data: {
|
||||
results: [
|
||||
{ id: 1, name: 'Mock Resource 1' },
|
||||
{ id: 2, name: 'Mock Resource 2' },
|
||||
{ id: 3, name: 'Mock Resource 3' },
|
||||
{ id: 4, name: 'Mock Resource 4' },
|
||||
{ id: 5, name: 'Mock Resource 5' },
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
dom = angular.element(`<lookup-modal>${listHtml}</lookup-modal>`);
|
||||
|
||||
// populate $templateCache with directive.templateUrl at test runtime,
|
||||
lookupTemplate = window.__html__['client/src/shared/lookup/lookup-modal.partial.html'];
|
||||
paginateTemplate = window.__html__['client/src/shared/paginate/paginate.partial.html'];
|
||||
searchTemplate = window.__html__['client/src/shared/smart-search/smart-search.partial.html'];
|
||||
columnSortTemplate = window.__html__['client/src/shared/column-sort/column-sort.partial.html'];
|
||||
|
||||
$templateCache.put('/static/partials/shared/lookup/lookup-modal.partial.html', lookupTemplate);
|
||||
$templateCache.put('/static/partials/shared/paginate/paginate.partial.html', paginateTemplate);
|
||||
$templateCache.put('/static/partials/shared/smart-search/smart-search.partial.html', searchTemplate);
|
||||
$templateCache.put('/static/partials/shared/column-sort/column-sort.partial.html', columnSortTemplate);
|
||||
|
||||
$compile = _$compile_;
|
||||
$parent = _$rootScope_.$new();
|
||||
|
||||
// mock resolvables
|
||||
$scope = $parent.$new();
|
||||
$scope.$resolve = {
|
||||
ListDefinition: listDefinition,
|
||||
Dataset: Dataset
|
||||
};
|
||||
}));
|
||||
|
||||
it('Resource is pre-selected in form - corresponding radio should initialize checked', () => {
|
||||
$parent.mock = 1; // resource id
|
||||
$parent.mock_name = 'Mock Resource 1'; // resource name
|
||||
|
||||
console.log($scope);
|
||||
|
||||
element = $compile(dom)($scope);
|
||||
$scope.$digest();
|
||||
|
||||
expect($(':radio')[0].is(':checked')).toEqual(true);
|
||||
});
|
||||
|
||||
it('No resource pre-selected in form - no radio should initialize checked', () => {
|
||||
element = $compile(dom)($scope);
|
||||
$scope.$digest();
|
||||
|
||||
_.forEach($(':radio'), (radio) => {
|
||||
expect(radio.is('checked')).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
it('Should update $parent / form scope and exit $state on save', () => {
|
||||
element = $compile(dom)($scope);
|
||||
$scope.$digest();
|
||||
$(':radio')[1].click();
|
||||
$('.Lookup-save')[0].click();
|
||||
|
||||
expect($parent.mock).toEqual(2);
|
||||
expect($parent.mock_name).toEqual('Mock Resource 2');
|
||||
expect($state.go).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('Should not update $parent / form scope on exit via header', () => {
|
||||
$parent.mock = 3; // resource id
|
||||
$parent.mock_name = 'Mock Resource 3'; // resource name
|
||||
element = $compile(dom)($scope);
|
||||
$scope.$digest();
|
||||
|
||||
$(':radio')[1].click();
|
||||
$('.Form-exit')[0].click();
|
||||
|
||||
expect($parent.mock).toEqual(3);
|
||||
expect($parent.mock_name).toEqual('Mock Resource 3');
|
||||
expect($state.go).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('Should not update $parent / form scope on exit via cancel button', () => {
|
||||
$parent.mock = 3; // resource id
|
||||
$parent.mock_name = 'Mock Resource 3'; // resource name
|
||||
element = $compile(dom)($scope);
|
||||
$scope.$digest();
|
||||
|
||||
$(':radio')[1].click();
|
||||
$('.Lookup-cancel')[0].click();
|
||||
|
||||
expect($parent.mock).toEqual(3);
|
||||
expect($parent.mock_name).toEqual('Mock Resource 3');
|
||||
expect($state.go).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
116
awx/ui/tests/spec/paginate/paginate.directive-test.js
Normal file
116
awx/ui/tests/spec/paginate/paginate.directive-test.js
Normal file
@@ -0,0 +1,116 @@
|
||||
'use strict';
|
||||
|
||||
xdescribe('Directive: Paginate', () => {
|
||||
var dom = angular.element('<paginate base-path="mock" dataset="mock_dataset" iterator="mock"></paginate>'),
|
||||
template,
|
||||
element,
|
||||
$scope,
|
||||
$compile,
|
||||
$state,
|
||||
$stateParams = {};
|
||||
|
||||
beforeEach(angular.mock.module('shared'), ($provide) =>{
|
||||
$provide.value('Rest', angular.noop);
|
||||
});
|
||||
beforeEach(angular.mock.module('PaginateModule', ($provide) => {
|
||||
$state = jasmine.createSpyObj('$state', ['go']);
|
||||
|
||||
$provide.value('$stateParams', $stateParams);
|
||||
$provide.value('Rest', angular.noop);
|
||||
}));
|
||||
beforeEach(angular.mock.inject(($templateCache, _$rootScope_, _$compile_) => {
|
||||
// populate $templateCache with directive.templateUrl at test runtime,
|
||||
template = window.__html__['client/src/shared/paginate/paginate.partial.html'];
|
||||
$templateCache.put('/static/partials/shared/paginate/paginate.partial.html', template);
|
||||
|
||||
$compile = _$compile_;
|
||||
$scope = _$rootScope_.$new();
|
||||
}));
|
||||
|
||||
it('should be hidden if only 1 page of data', () => {
|
||||
|
||||
$scope.mock_dataset = {count: 19};
|
||||
$scope.pageSize = 20;
|
||||
element = $compile(dom)($scope);
|
||||
$scope.$digest();
|
||||
|
||||
expect($('.Paginate-wrapper', element)).hasClass('ng-hide');
|
||||
});
|
||||
describe('it should show expected page range', () => {
|
||||
|
||||
|
||||
it('should show 7 pages', () =>{
|
||||
|
||||
$scope.pageSize = 1;
|
||||
$scope.mock_dataset = {count: 7};
|
||||
element = $compile(dom)($scope);
|
||||
$scope.$digest();
|
||||
// next, previous, 7 pages
|
||||
expect($('.Paginate-controls--item', element)).length.toEqual(9);
|
||||
});
|
||||
it('should show 100 pages', () =>{
|
||||
$scope.pageSize = 1;
|
||||
$scope.mock_dataset = {count: 100};
|
||||
element = $compile(dom)($scope);
|
||||
$scope.$digest();
|
||||
// first, next, previous, last, 100 pages
|
||||
expect($('.Paginate-controls--item', element)).length.toEqual(104);
|
||||
});
|
||||
});
|
||||
describe('it should get expected page', () => {
|
||||
|
||||
it('should get the next page', () =>{
|
||||
|
||||
$scope.mock_dataset = {
|
||||
count: 42,
|
||||
};
|
||||
|
||||
$stateParams.mock_search = {
|
||||
page_size: 5,
|
||||
page: 1
|
||||
};
|
||||
|
||||
element = $compile(dom)($scope);
|
||||
$('.Paginate-controls--next').click();
|
||||
expect($stateParams.mock_search.page).toEqual(2);
|
||||
});
|
||||
|
||||
it('should get the previous page', ()=>{
|
||||
|
||||
$scope.mock_dataset = {
|
||||
count: 42
|
||||
};
|
||||
$stateParams.mock_search = {
|
||||
page_size: 10,
|
||||
page: 3
|
||||
};
|
||||
|
||||
element = $compile(dom)($scope);
|
||||
$('.Paginate-controls--previous');
|
||||
expect($stateParams.mock_search.page).toEqual(2);
|
||||
});
|
||||
it('should get the last page', ()=>{
|
||||
$scope.mock_dataset = {
|
||||
count: 110
|
||||
};
|
||||
$stateParams.mock_search = {
|
||||
page_size: 5,
|
||||
page: 1
|
||||
};
|
||||
$('.Paginate-controls--last').click();
|
||||
expect($stateParams.mock_search.page).toEqual(42);
|
||||
});
|
||||
it('should get the first page', () => {
|
||||
$scope.mock_dataset = {
|
||||
count: 110
|
||||
};
|
||||
$stateParams.mock_search = {
|
||||
page_size: 5,
|
||||
page: 35
|
||||
};
|
||||
$('.Paginate-controls--first').click();
|
||||
expect($stateParams.mock_search.page).toEqual(1);
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
85
awx/ui/tests/spec/smart-search/queryset.service-test.js
Normal file
85
awx/ui/tests/spec/smart-search/queryset.service-test.js
Normal file
@@ -0,0 +1,85 @@
|
||||
'use strict';
|
||||
|
||||
describe('Service: QuerySet', () => {
|
||||
let $httpBackend,
|
||||
QuerySet,
|
||||
Authorization;
|
||||
|
||||
beforeEach(angular.mock.module('Tower', ($provide) =>{
|
||||
// @todo: improve app source / write testing utilities for interim
|
||||
// we don't want to be concerned with this provision in every test that involves the Rest module
|
||||
Authorization = {
|
||||
getToken: () => true,
|
||||
isUserLoggedIn: angular.noop
|
||||
};
|
||||
$provide.value('LoadBasePaths', angular.noop);
|
||||
$provide.value('Authorization', Authorization);
|
||||
}));
|
||||
beforeEach(angular.mock.module('RestServices'));
|
||||
|
||||
beforeEach(angular.mock.inject((_$httpBackend_, _QuerySet_) => {
|
||||
$httpBackend = _$httpBackend_;
|
||||
QuerySet = _QuerySet_;
|
||||
|
||||
// @todo: improve app source
|
||||
// config.js / local_settings emit $http requests in the app's run block
|
||||
$httpBackend
|
||||
.whenGET(/\/static\/*/)
|
||||
.respond(200, {});
|
||||
// @todo: improve appsource
|
||||
// provide api version via package.json config block
|
||||
$httpBackend
|
||||
.whenGET('/api/')
|
||||
.respond(200, '');
|
||||
}));
|
||||
|
||||
describe('fn encodeQuery', () => {
|
||||
xit('null/undefined params should return an empty string', () => {
|
||||
expect(QuerySet.encodeQuery(null)).toEqual('');
|
||||
expect(QuerySet.encodeQuery(undefined)).toEqual('');
|
||||
});
|
||||
xit('should encode params to a string', () => {
|
||||
let params = {
|
||||
or__created_by: 'Jenkins',
|
||||
or__modified_by: 'Jenkins',
|
||||
and__not__status: 'success',
|
||||
},
|
||||
result = '?or__created_by=Jenkins&or__modified_by=Jenkins&and__not__status=success';
|
||||
expect(QuerySet.encodeQuery(params)).toEqual(result);
|
||||
});
|
||||
});
|
||||
|
||||
xdescribe('fn decodeQuery', () => {
|
||||
|
||||
});
|
||||
|
||||
|
||||
describe('fn search', () => {
|
||||
let pattern = /\/api\/v1\/inventories\/(.+)\/groups\/*/,
|
||||
endpoint = '/api/v1/inventories/1/groups/',
|
||||
params = {
|
||||
or__name: 'borg',
|
||||
or__description__icontains: 'assimilate'
|
||||
};
|
||||
|
||||
it('should GET expected URL', () =>{
|
||||
$httpBackend
|
||||
.expectGET(pattern)
|
||||
.respond(200, {});
|
||||
QuerySet.search(endpoint, params).then((data) =>{
|
||||
expect(data.config.url).toEqual('/api/v1/inventories/1/groups/?or__name=borg&or__description__icontains=assimilate');
|
||||
});
|
||||
$httpBackend.flush();
|
||||
});
|
||||
|
||||
xit('should memoize new DjangoModel', ()=>{});
|
||||
xit('should not replace memoized DjangoModel', ()=>{});
|
||||
xit('should provide an alias interface', ()=>{});
|
||||
|
||||
afterEach(() => {
|
||||
$httpBackend.verifyNoOutstandingExpectation();
|
||||
$httpBackend.verifyNoOutstandingRequest();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
163
awx/ui/tests/spec/smart-search/smart-search.directive-test.js
Normal file
163
awx/ui/tests/spec/smart-search/smart-search.directive-test.js
Normal file
@@ -0,0 +1,163 @@
|
||||
'use strict';
|
||||
|
||||
xdescribe('Directive: Smart Search', () => {
|
||||
let $scope,
|
||||
template,
|
||||
element,
|
||||
dom,
|
||||
$compile,
|
||||
$state = {},
|
||||
$stateParams,
|
||||
GetBasePath,
|
||||
QuerySet;
|
||||
|
||||
beforeEach(angular.mock.module('shared'));
|
||||
beforeEach(angular.mock.module('SmartSearchModule', ($provide) => {
|
||||
QuerySet = jasmine.createSpyObj('QuerySet', ['decodeParam']);
|
||||
QuerySet.decodeParam.and.callFake((key, value) => {
|
||||
return `${key.split('__').join(':')}:${value}`;
|
||||
});
|
||||
GetBasePath = jasmine.createSpy('GetBasePath');
|
||||
|
||||
$provide.value('QuerySet', QuerySet);
|
||||
$provide.value('GetBasePath', GetBasePath);
|
||||
$provide.value('$state', $state);
|
||||
}));
|
||||
beforeEach(angular.mock.inject(($templateCache, _$rootScope_, _$compile_) => {
|
||||
// populate $templateCache with directive.templateUrl at test runtime,
|
||||
template = window.__html__['client/src/shared/smart-search/smart-search.partial.html'];
|
||||
$templateCache.put('/static/partials/shared/smart-search/smart-search.partial.html', template);
|
||||
|
||||
$compile = _$compile_;
|
||||
$scope = _$rootScope_.$new();
|
||||
}));
|
||||
|
||||
describe('initializing tags', () => {
|
||||
beforeEach(() => {
|
||||
QuerySet.initFieldset = function() {
|
||||
return {
|
||||
then: function() {
|
||||
return;
|
||||
}
|
||||
};
|
||||
};
|
||||
});
|
||||
// some defaults like page_size and page will always be provided
|
||||
// but should be squashed if initialized with default values
|
||||
it('should not create tags', () => {
|
||||
$state.$current = {
|
||||
params: {
|
||||
mock_search: {
|
||||
config: {
|
||||
value: {
|
||||
page_size: '20',
|
||||
order_by: '-finished',
|
||||
page: '1'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
$state.params = {
|
||||
mock_search: {
|
||||
page_size: '20',
|
||||
order_by: '-finished',
|
||||
page: '1'
|
||||
}
|
||||
};
|
||||
dom = angular.element(`<smart-search
|
||||
django-model="mock"
|
||||
search-size="mock"
|
||||
base-path="mock"
|
||||
iterator="mock"
|
||||
collection="dataset"
|
||||
search-tags="searchTags"
|
||||
>
|
||||
</smart-search>`);
|
||||
element = $compile(dom)($scope);
|
||||
$scope.$digest();
|
||||
expect($('.SmartSearch-tagContainer', element).length).toEqual(0);
|
||||
});
|
||||
// set one possible default (order_by) with a custom value, but not another default (page_size)
|
||||
it('should create an order_by tag, but not a page_size tag', () => {
|
||||
$state.$current = {
|
||||
params: {
|
||||
mock_search: {
|
||||
config: {
|
||||
value: {
|
||||
page_size: '20',
|
||||
order_by: '-finished'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
$state.params = {
|
||||
mock_search: {
|
||||
page_size: '20',
|
||||
order_by: 'name'
|
||||
}
|
||||
};
|
||||
dom = angular.element(`<smart-search
|
||||
django-model="mock"
|
||||
search-size="mock"
|
||||
base-path="mock"
|
||||
iterator="mock"
|
||||
collection="dataset"
|
||||
search-tags="searchTags"
|
||||
>
|
||||
</smart-search>`);
|
||||
element = $compile(dom)($scope);
|
||||
$scope.$digest();
|
||||
expect($('.SmartSearch-tagContainer', element).length).toEqual(1);
|
||||
expect($('.SmartSearch-tagContainer .SmartSearch-name', element)[0].innerText).toEqual('order_by:name');
|
||||
});
|
||||
// set many possible defaults and many non-defaults - page_size and page shouldn't generate tags, even when non-default values are set
|
||||
it('should create an order_by tag, name tag, description tag - but not a page_size or page tag', () => {
|
||||
$state.$current = {
|
||||
params: {
|
||||
mock_search: {
|
||||
config: {
|
||||
value: {
|
||||
page_size: '20',
|
||||
order_by: '-finished',
|
||||
page: '1'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
$state.params = {
|
||||
mock_search: {
|
||||
page_size: '25',
|
||||
order_by: 'name',
|
||||
page: '11',
|
||||
description_icontains: 'ansible',
|
||||
name_icontains: 'ansible'
|
||||
}
|
||||
};
|
||||
dom = angular.element(`<smart-search
|
||||
django-model="mock"
|
||||
search-size="mock"
|
||||
base-path="mock"
|
||||
iterator="mock"
|
||||
collection="dataset"
|
||||
search-tags="searchTags"
|
||||
>
|
||||
</smart-search>`);
|
||||
element = $compile(dom)($scope);
|
||||
$scope.$digest();
|
||||
expect($('.SmartSearch-tagContainer', element).length).toEqual(3);
|
||||
});
|
||||
});
|
||||
|
||||
describe('removing tags', () => {
|
||||
// assert a default value is still provided after a custom tag is removed
|
||||
xit('should revert to state-defined order_by when order_by tag is removed', () => {});
|
||||
});
|
||||
|
||||
describe('accessing model', () => {
|
||||
xit('should retrieve cached model OPTIONS from localStorage', () => {});
|
||||
xit('should call QuerySet service to retrieve unstored model OPTIONS', () => {});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user