diff --git a/app/assets/javascripts/angular/config/work-packages-config.js b/app/assets/javascripts/angular/config/work-packages-config.js index 2b3bed5a7e..3f807db6e0 100644 --- a/app/assets/javascripts/angular/config/work-packages-config.js +++ b/app/assets/javascripts/angular/config/work-packages-config.js @@ -28,7 +28,7 @@ angular.module('openproject.workPackages.config') -.constant('INITIALLY_SELECTED_COLUMNS', ['id', 'project', 'type', 'status', 'priority', 'subject', 'assigned_to_id', 'updated_at']) +.constant('INITIALLY_SELECTED_COLUMNS', [{ name: 'id' }, { name: 'project' }, { name: 'type' }, { name: 'status' }, { name: 'priority' }, { name: 'subject' }, { name: 'assigned_to_id' }, { name: 'updated_at' }]) .constant('OPERATORS_AND_LABELS_BY_FILTER_TYPE', { list: [['=', 'label_equals'], ['!', 'label_not_equals']], diff --git a/app/assets/javascripts/angular/controllers/dialogs/save.js b/app/assets/javascripts/angular/controllers/dialogs/save.js index 969ee586da..3840bc0b7b 100644 --- a/app/assets/javascripts/angular/controllers/dialogs/save.js +++ b/app/assets/javascripts/angular/controllers/dialogs/save.js @@ -52,7 +52,7 @@ angular.module('openproject.workPackages.controllers') .then(function(data){ // push query id to URL without reinitializing work-packages-list-controller if (data.query) { - $state.go('work-packages.list', { query_id: data.query.id }, { notify: false }); + $state.go('work-packages.list', { query_id: data.query.id, query: null }, { notify: false }); AuthorisationService.initModelAuth("query", data.query._links); } diff --git a/app/assets/javascripts/angular/directives/components/table-pagination-directive.js b/app/assets/javascripts/angular/directives/components/table-pagination-directive.js index 08759de63d..89c769b433 100644 --- a/app/assets/javascripts/angular/directives/components/table-pagination-directive.js +++ b/app/assets/javascripts/angular/directives/components/table-pagination-directive.js @@ -33,8 +33,7 @@ angular.module('openproject.uiComponents') restrict: 'EA', templateUrl: '/templates/components/table_pagination.html', scope: { - totalEntries: '=', - updateResults: '&' + totalEntries: '=' }, link: function(scope, element, attributes) { scope.I18n = I18n; @@ -53,7 +52,7 @@ angular.module('openproject.uiComponents') updateCurrentRangeLabel(); updatePageNumbers(); - scope.updateResults(); // update table + scope.$emit('workPackagesRefreshRequired'); }; /** diff --git a/app/assets/javascripts/angular/helpers/url-params-helper.js b/app/assets/javascripts/angular/helpers/url-params-helper.js index ada4f4757f..7582892643 100644 --- a/app/assets/javascripts/angular/helpers/url-params-helper.js +++ b/app/assets/javascripts/angular/helpers/url-params-helper.js @@ -51,6 +51,88 @@ angular.module('openproject.helpers') return parts.join('&'); }, + encodeQueryJsonParams: function(query) { + var paramsData = { + c: query.columns.map(function(column) { return column.name; }) + }; + if(!!query.displaySums) { + paramsData.s = query.displaySums; + } + + if(!!query.projectId) { + paramsData.p = query.projectId; + } + if(!!query.groupBy) { + paramsData.g = query.groupBy; + } + if(!!query.getSortation()) { + paramsData.t = query.getSortation().encode() + } + if(query.filters && query.filters.length) { + paramsData.f = query.filters.filter(function(filter) { + return !filter.deactivated; + }) + .map(function(filter) { + var filterData = { + n: filter.name, + m: filter.modelName, + o: encodeURIComponent(filter.operator), + t: filter.type + }; + if(filter.values) { + angular.extend(filterData, { v: filter.values }) + } + return filterData + }); + } + + return JSON.stringify(paramsData); + }, + + decodeQueryFromJsonParams: function(queryId, updateJson) { + var queryData = {}; + if(queryId) { + queryData.id = queryId; + } + + if(updateJson) { + var properties = JSON.parse(updateJson); + + if(!!properties.c) { + queryData.columns = properties.c.map(function(column) { return { name: column }; }); + } + if(!!properties.s) { + queryData.displaySums = properties.s; + } + if(!!properties.p) { + queryData.projectId = properties.p; + } + if(!!properties.g) { + queryData.groupBy = properties.g; + } + if(!!properties.f) { + queryData.filters = properties.f.map(function(urlFilter) { + var filterData = { + name: urlFilter.n, + modelName: urlFilter.m, + operator: decodeURIComponent(urlFilter.o), + type: urlFilter.t + }; + if(urlFilter.v) { + var vs = Array.isArray(urlFilter.v) ? urlFilter.v : [urlFilter.v]; + angular.extend(filterData, { values: vs }); + } + return filterData; + }); + } + if(!!properties.t) { + queryData.sortCriteria = properties.t; + } + } + + return queryData; + }, + buildQueryExportOptions: function(query){ var relativeUrl = "/work_packages"; if (query.project_id){ diff --git a/app/assets/javascripts/angular/models/query.js b/app/assets/javascripts/angular/models/query.js index ba37562dc8..060b6ecb65 100644 --- a/app/assets/javascripts/angular/models/query.js +++ b/app/assets/javascripts/angular/models/query.js @@ -31,7 +31,8 @@ angular.module('openproject.models') .factory('Query', ['Filter', 'Sortation', 'UrlParamsHelper', - function(Filter, Sortation, UrlParamsHelper) { + 'INITIALLY_SELECTED_COLUMNS', + function(Filter, Sortation, UrlParamsHelper, INITIALLY_SELECTED_COLUMNS) { Query = function (queryData, options) { angular.extend(this, queryData, options); @@ -39,7 +40,19 @@ angular.module('openproject.models') this.filters = []; this.groupBy = this.groupBy || ''; - if(queryData) this.setFilters(queryData.filters); + if(queryData.filters){ + if(options && options.rawFilters) { + this.setRawFilters(queryData.filters); + } else { + this.setFilters(queryData.filters); + } + } + + if(queryData.sortCriteria) this.setSortation(queryData.sortCriteria); + + if(!this.columns) { + this.setColumns(INITIALLY_SELECTED_COLUMNS); + } }; Query.prototype = { @@ -56,7 +69,7 @@ angular.module('openproject.models') 'f[]': this.getFilterNames(this.getActiveConfiguredFilters()), 'c[]': this.getParamColumns(), 'group_by': this.groupBy, - 'sort': this.sortation.encode(), + 'sort': this.getEncodedSortation(), 'display_sums': this.displaySums, 'name': this.name, 'is_public': this.isPublic @@ -74,7 +87,7 @@ angular.module('openproject.models') 'f[]': this.getFilterNames(this.getActiveConfiguredFilters()), 'c[]': this.getParamColumns(), 'group_by': this.groupBy, - 'sort': this.sortation.encode(), + 'sort': this.getEncodedSortation(), 'display_sums': this.displaySums, 'name': this.name, 'is_public': this.isPublic @@ -87,6 +100,7 @@ angular.module('openproject.models') save: function(data){ // Note: query has already been updated, only the id needs to be set this.id = data.id; + this.dirty = false; return this; }, @@ -98,6 +112,19 @@ angular.module('openproject.models') this.starred = false; }, + update: function(queryData) { + angular.extend(this, queryData); + + if(queryData.filters){ + this.filters = []; + this.setRawFilters(queryData.filters); + } + if(queryData.sortCriteria) this.setSortation(queryData.sortCriteria); + this.dirty = true; + + return this; + }, + getQueryString: function(){ return UrlParamsHelper.buildQueryString(this.toParams()); }, @@ -106,8 +133,8 @@ angular.module('openproject.models') return this.sortation; }, - setSortation: function(sortation){ - this.sortation = sortation; + setSortation: function(sortCriteria){ + this.sortation = new Sortation(sortCriteria); }, setGroupBy: function(groupBy) { @@ -164,6 +191,20 @@ angular.module('openproject.models') } }, + setRawFilters: function(filters) { + if (filters){ + var self = this; + + this.filters = filters.map(function(filterData){ + return new Filter(filterData); + }); + } + }, + + setColumns: function(columns) { + this.columns = columns; + }, + /** * @name isDefault * @function @@ -224,6 +265,10 @@ angular.module('openproject.models') return selectedColumns; }, + getEncodedSortation: function() { + return !!this.sortation ? this.sortation.encode() : null; + }, + getColumnNames: function() { return this.columns.map(function(column) { return column.name; @@ -294,10 +339,14 @@ angular.module('openproject.models') }); }, - isNew: function(){ + isNew: function() { return !this.id; }, + isDirty: function() { + return this.isNew() || this.dirty; + }, + hasName: function() { return !!this.name && !this.isDefault(); }, diff --git a/app/assets/javascripts/angular/services/query-service.js b/app/assets/javascripts/angular/services/query-service.js index 08bb1b2c9f..e946390433 100644 --- a/app/assets/javascripts/angular/services/query-service.js +++ b/app/assets/javascripts/angular/services/query-service.js @@ -74,14 +74,15 @@ angular.module('openproject.services') starred: queryData.starred, links: queryData._links }); - query.setSortation(new Sortation(queryData.sort_criteria)); + query.setSortation(queryData.sort_criteria); QueryService.getAvailableFilters(query.project_id) .then(function(availableFilters) { query.setAvailableWorkPackageFilters(availableFilters); if (query.isDefault()) { query.setDefaultFilter(); - } else { + } + if(queryData.filters && queryData.filters.length) { query.setFilters(queryData.filters); } @@ -92,10 +93,45 @@ angular.module('openproject.services') return query; }, + updateQuery: function(values, afterUpdate) { + var queryData = { + }; + if(!!values.display_sums) { + queryData.displaySums = values.display_sums; + } + if(!!values.columns) { + queryData.columns = values.columns; + } + if(!!values.group_by) { + queryData.groupBy = values.group_by; + } + if(!!values.sort_criteria) { + queryData.sortCriteria = values.sort_criteria; + } + query.update(queryData); + + QueryService.getAvailableFilters(query.project_id) + .then(function(availableFilters) { + query.setAvailableWorkPackageFilters(availableFilters); + if(queryData.filters && queryData.filters.length) { + query.setFilters(queryData.filters); + } + + return query; + }) + .then(afterUpdate); + + return query; + }, + getQuery: function() { return query; }, + clearQuery: function() { + query = null; + }, + getQueryName: function() { if (query && query.hasName()) { return query.name; diff --git a/app/assets/javascripts/angular/services/work-package-service.js b/app/assets/javascripts/angular/services/work-package-service.js index 925b999878..3a514a30d9 100644 --- a/app/assets/javascripts/angular/services/work-package-service.js +++ b/app/assets/javascripts/angular/services/work-package-service.js @@ -36,7 +36,8 @@ angular.module('openproject.services') 'WorkPackagesHelper', 'HALAPIResource', 'DEFAULT_FILTER_PARAMS', - function($http, PathHelper, WorkPackagesHelper, HALAPIResource, DEFAULT_FILTER_PARAMS) { + 'DEFAULT_PAGINATION_OPTIONS', + function($http, PathHelper, WorkPackagesHelper, HALAPIResource, DEFAULT_FILTER_PARAMS, DEFAULT_PAGINATION_OPTIONS) { var workPackage; var WorkPackageService = { @@ -56,20 +57,22 @@ angular.module('openproject.services') return WorkPackageService.doQuery(url, params); }, - getWorkPackagesFromUrlQueryParams: function(projectIdentifier, location) { + getWorkPackages: function(projectIdentifier, query, paginationOptions) { var url = projectIdentifier ? PathHelper.apiProjectWorkPackagesPath(projectIdentifier) : PathHelper.apiWorkPackagesPath(); var params = {}; - angular.extend(params, location.search()); - return WorkPackageService.doQuery(url, params); - }, + if(query) { + angular.extend(params, query.toUpdateParams()); + } - getWorkPackages: function(projectIdentifier, query, paginationOptions) { - var url = projectIdentifier ? PathHelper.apiProjectWorkPackagesPath(projectIdentifier) : PathHelper.apiWorkPackagesPath(); - var params = angular.extend(query.toUpdateParams(), { - page: paginationOptions.page, - per_page: paginationOptions.perPage - }); + if(paginationOptions) { + angular.extend(params, { + page: paginationOptions.page, + per_page: paginationOptions.perPage + }); + } else { + angular.extend(params, DEFAULT_PAGINATION_OPTIONS); + } return WorkPackageService.doQuery(url, params); }, diff --git a/app/assets/javascripts/angular/work_packages/controllers/work-packages-list-controller.js b/app/assets/javascripts/angular/work_packages/controllers/work-packages-list-controller.js index f7640590fe..be6da7efe2 100644 --- a/app/assets/javascripts/angular/work_packages/controllers/work-packages-list-controller.js +++ b/app/assets/javascripts/angular/work_packages/controllers/work-packages-list-controller.js @@ -31,10 +31,8 @@ angular.module('openproject.workPackages.controllers') .controller('WorkPackagesListController', [ '$scope', '$rootScope', - '$q', - '$location', - '$stateParams', '$state', + '$location', 'latestTab', 'I18n', 'WorkPackagesTableService', @@ -43,14 +41,12 @@ angular.module('openproject.workPackages.controllers') 'QueryService', 'PaginationService', 'AuthorisationService', - 'WorkPackageLoadingHelper', - 'HALAPIResource', - 'INITIALLY_SELECTED_COLUMNS', + 'UrlParamsHelper', 'OPERATORS_AND_LABELS_BY_FILTER_TYPE', - function($scope, $rootScope, $q, $location, $stateParams, $state, latestTab, + function($scope, $rootScope, $state, $location, latestTab, I18n, WorkPackagesTableService, WorkPackageService, ProjectService, QueryService, PaginationService, - AuthorisationService, WorkPackageLoadingHelper, HALAPIResource, INITIALLY_SELECTED_COLUMNS, + AuthorisationService, UrlParamsHelper, OPERATORS_AND_LABELS_BY_FILTER_TYPE) { @@ -59,12 +55,19 @@ angular.module('openproject.workPackages.controllers') $scope.operatorsAndLabelsByFilterType = OPERATORS_AND_LABELS_BY_FILTER_TYPE; $scope.disableFilters = false; $scope.disableNewWorkPackage = true; + var queryParams = $location.search().query_props; var fetchWorkPackages; - if($scope.query_id){ - fetchWorkPackages = WorkPackageService.getWorkPackagesByQueryId($scope.projectIdentifier, $scope.query_id); + if(queryParams) { + // Attempt to build up query from URL params + fetchWorkPackages = fetchWorkPackagesFromUrlParams(queryParams); + } else if($state.params.query_id) { + // Load the query by id if present + fetchWorkPackages = WorkPackageService.getWorkPackagesByQueryId($scope.projectIdentifier, $state.params.query_id); } else { - fetchWorkPackages = WorkPackageService.getWorkPackagesFromUrlQueryParams($scope.projectIdentifier, $location); + // Clear the cached query and load the default + QueryService.clearQuery(); + fetchWorkPackages = WorkPackageService.getWorkPackages($scope.projectIdentifier); } $scope.settingUpPage = fetchWorkPackages // put promise in scope for cg-busy @@ -76,6 +79,28 @@ angular.module('openproject.workPackages.controllers') }); } + function fetchWorkPackagesFromUrlParams(queryParams) { + try { + var queryData = UrlParamsHelper.decodeQueryFromJsonParams($state.params.query_id, queryParams); + var queryFromParams = new Query(queryData, { rawFilters: true }); + + return WorkPackageService.getWorkPackages($scope.projectIdentifier, queryFromParams); + } catch(e) { + $scope.$emit('flashMessage', { + isError: true, + text: I18n.t('js.work_packages.query.errors.unretrievable_query') + }); + clearUrlQueryParams(); + + return WorkPackageService.getWorkPackages($scope.projectIdentifier); + } + } + + function clearUrlQueryParams() { + $location.search('query_props', null); + $location.search('query_id', null); + } + function fetchProjectTypesAndQueries() { if ($scope.projectIdentifier) { ProjectService.getProject($scope.projectIdentifier) @@ -98,21 +123,24 @@ angular.module('openproject.workPackages.controllers') } function initQuery(metaData) { - var storedQuery = QueryService.getQuery(); + var queryData = metaData.query, + columnData = metaData.columns; - if (storedQuery && $stateParams.query_id !== null && storedQuery.id === $scope.query_id) { - $scope.query = storedQuery; - } else { - var queryData = metaData.query, - columnData = metaData.columns; + var cachedQuery = QueryService.getQuery(); + var urlQueryId = $state.params.query_id; - $scope.query = QueryService.initQuery($scope.query_id, queryData, columnData, metaData.export_formats, afterQuerySetupCallback); + if (cachedQuery && urlQueryId && cachedQuery.id == urlQueryId) { + // Augment current unsaved query with url param data + var updateData = angular.extend(queryData, { columns: columnData }) + $scope.query = QueryService.updateQuery(updateData, afterQuerySetupCallback); + } else { + // Set up fresh query from retrieved query meta data + $scope.query = QueryService.initQuery($state.params.query_id, queryData, columnData, metaData.export_formats, afterQuerySetupCallback); } } function afterQuerySetupCallback(query) { $scope.showFiltersOptions = query.filters.length > 0; - $scope.updateBackUrl(); } function setupWorkPackagesTable(json) { @@ -145,9 +173,6 @@ angular.module('openproject.workPackages.controllers') $scope.workPackageCountByGroup = meta.work_package_count_by_group; $scope.totalEntries = QueryService.getTotalEntries(); - // back url - $scope.updateBackUrl(); - // Authorisation AuthorisationService.initModelAuth("work_package", meta._links); AuthorisationService.initModelAuth("query", meta.query._links); @@ -162,21 +187,30 @@ angular.module('openproject.workPackages.controllers') // Updates - $scope.updateBackUrl = function(){ - // Easier than trying to extract it from $location + $scope.maintainUrlQueryState = function(){ var relativeUrl = "/work_packages"; if ($scope.projectIdentifier){ relativeUrl = "/projects/" + $scope.projectIdentifier + relativeUrl; } - if($scope.query){ - relativeUrl = relativeUrl + "#?" + $scope.query.getQueryString(); + if($scope.query) { + var queryString = UrlParamsHelper.encodeQueryJsonParams($scope.query); + $location.search('query_props', queryString); + relativeUrl = relativeUrl + "?query_props=" + queryString; } $scope.backUrl = relativeUrl; }; - $scope.updateResults = function() { + $scope.loadQuery = function(queryId) { + // Clear unsaved changes to current query + clearUrlQueryParams(); + + // Load new query + $state.go('work-packages.list', { query_id: queryId }); + }; + + function updateResults() { $scope.$broadcast('openproject.workPackages.updateResults'); $scope.refreshWorkPackages = WorkPackageService.getWorkPackages($scope.projectIdentifier, $scope.query, PaginationService.getPaginationOptions()) @@ -185,10 +219,6 @@ angular.module('openproject.workPackages.controllers') return $scope.refreshWorkPackages; }; - $scope.setQueryState = function(query_id) { - $state.go('work-packages.list', { query_id: query_id }); - }; - // More function serviceErrorHandler(data) { @@ -208,6 +238,23 @@ angular.module('openproject.workPackages.controllers') $scope.selectedTitle = queryName || I18n.t('js.toolbar.unselected_title'); }); + $rootScope.$on('queryStateChange', function(event, message) { + $scope.maintainUrlQueryState(); + }); + + $rootScope.$on('workPackagesRefreshRequired', function(event, message) { + updateResults(); + }); + + $rootScope.$on('queryClearRequired', function(event, message) { + $location.search('query_props', null); + if($location.search().query_id) { + $location.search('query_id', null); + } else { + initialSetup(); + } + }); + $scope.openLatestTab = function() { $state.go(latestTab.getStateName(), { workPackageId: $scope.preselectedWorkPackageId }); }; diff --git a/app/assets/javascripts/angular/work_packages/directives/filter-clear-directive.js b/app/assets/javascripts/angular/work_packages/directives/filter-clear-directive.js index 30add2f591..68e12ab43c 100644 --- a/app/assets/javascripts/angular/work_packages/directives/filter-clear-directive.js +++ b/app/assets/javascripts/angular/work_packages/directives/filter-clear-directive.js @@ -29,18 +29,18 @@ angular.module('openproject.workPackages.directives') .directive('filterClear', [ - '$state', + '$location', 'I18n', 'QueryService', - function($state, I18n, QueryService){ + function($location, I18n, QueryService){ return { restrict: 'E', templateUrl: '/templates/work_packages/filter_clear.html', scope: true, link: function(scope, element, attributes) { scope.I18n = I18n; - scope.clearQuery = function(){ - $state.go('work-packages.list', { query_id: null }, { reload: true }); + scope.clearQuery = function() { + scope.$emit('queryClearRequired') }; } }; diff --git a/app/assets/javascripts/angular/work_packages/directives/options-dropdown-directive.js b/app/assets/javascripts/angular/work_packages/directives/options-dropdown-directive.js index 58dd4806a8..a075193f5f 100644 --- a/app/assets/javascripts/angular/work_packages/directives/options-dropdown-directive.js +++ b/app/assets/javascripts/angular/work_packages/directives/options-dropdown-directive.js @@ -61,6 +61,7 @@ angular.module('openproject.workPackages.directives') QueryService.saveQuery() .then(function(data){ scope.$emit('flashMessage', data.status); + $state.go('work-packages.list', { query_id: scope.query.id, query: null }, { notify: false }); }); } } @@ -72,7 +73,7 @@ angular.module('openproject.workPackages.directives') .then(function(data){ settingsModal.deactivate(); scope.$emit('flashMessage', data.status); - $state.go('work-packages.list', { query_id: null }, { reload: true }); + $state.go('work-packages.list', { query_id: null, query: null }, { reload: true }); }); } }; diff --git a/app/assets/javascripts/angular/work_packages/directives/query-columns-directive.js b/app/assets/javascripts/angular/work_packages/directives/query-columns-directive.js index 72108f1332..e50b4f4f5d 100644 --- a/app/assets/javascripts/angular/work_packages/directives/query-columns-directive.js +++ b/app/assets/javascripts/angular/work_packages/directives/query-columns-directive.js @@ -76,7 +76,7 @@ angular.module('openproject.workPackages.directives') var newColumns = WorkPackagesTableHelper.selectColumnsByName(scope.columns, columnNames); WorkPackageService.augmentWorkPackagesWithColumnsData(workPackages, newColumns, groupBy) - .then(scope.updateBackUrl); + .then(function(){ scope.$emit('queryStateChange') }); } } }; diff --git a/app/assets/javascripts/angular/work_packages/directives/query-filter-directive.js b/app/assets/javascripts/angular/work_packages/directives/query-filter-directive.js index 66932e1852..2feb8abc11 100644 --- a/app/assets/javascripts/angular/work_packages/directives/query-filter-directive.js +++ b/app/assets/javascripts/angular/work_packages/directives/query-filter-directive.js @@ -64,9 +64,8 @@ angular.module('openproject.workPackages.directives') if (filter !== oldFilter) { if (filter.isConfigured() && (filterChanged(filter, oldFilter) || valueReset(filter, oldFilter))) { PaginationService.resetPage(); - - applyFilters(); - scope.updateBackUrl(); + scope.$emit('queryStateChange'); + scope.$emit('workPackagesRefreshRequired'); } } }, true); diff --git a/app/assets/javascripts/angular/work_packages/directives/query-filters-directive.js b/app/assets/javascripts/angular/work_packages/directives/query-filters-directive.js index d9b70a30c8..1c0b66189b 100644 --- a/app/assets/javascripts/angular/work_packages/directives/query-filters-directive.js +++ b/app/assets/javascripts/angular/work_packages/directives/query-filters-directive.js @@ -43,7 +43,6 @@ angular.module('openproject.workPackages.directives') if (filterName) { scope.query.addFilter(filterName); scope.filterToBeAdded = undefined; - scope.updateBackUrl(); } }); diff --git a/app/assets/javascripts/angular/work_packages/directives/query-form-directive.js b/app/assets/javascripts/angular/work_packages/directives/query-form-directive.js index 437004f7fd..7e6af7f31d 100644 --- a/app/assets/javascripts/angular/work_packages/directives/query-form-directive.js +++ b/app/assets/javascripts/angular/work_packages/directives/query-form-directive.js @@ -55,6 +55,15 @@ angular.module('openproject.workPackages.directives') return groupByChanged || sortElementsChanged; } + function passiveQueryPropertiesChanged(currentProperties, formerProperties) { + if (formerProperties === undefined) return false; + + var columnsChanged = JSON.stringify(currentProperties.columns) !== JSON.stringify(formerProperties.columns); + var displaySumsChanged = currentProperties.displaySums !== formerProperties.displaySums; + + return columnsChanged || displaySumsChanged; + } + function observedQueryProperties() { var query = scope.query; @@ -69,14 +78,31 @@ angular.module('openproject.workPackages.directives') } } + function passiveQueryProperties() { + var query = scope.query; + + if (query !== undefined) { + return { + columns: query.columns, + displaySums: query.displaySums + }; + } + } + scope.$watch(observedQueryProperties, function(newProperties, oldProperties) { if (!querySwitched(newProperties, oldProperties)) { if (queryPropertiesChanged(newProperties, oldProperties)) { - scope.updateResults(); - scope.updateBackUrl(); + scope.$emit('queryStateChange'); + scope.$emit('workPackagesRefreshRequired'); } } }, true); + + scope.$watch(passiveQueryProperties, function(newProperties, oldProperties) { + if (passiveQueryPropertiesChanged(newProperties, oldProperties)) { + scope.$emit('queryStateChange'); + } + }, true); } }; } diff --git a/app/assets/javascripts/angular/work_packages/directives/work-package-group-sums-directive.js b/app/assets/javascripts/angular/work_packages/directives/work-package-group-sums-directive.js index dbc1902a01..1f69625c3c 100644 --- a/app/assets/javascripts/angular/work_packages/directives/work-package-group-sums-directive.js +++ b/app/assets/javascripts/angular/work_packages/directives/work-package-group-sums-directive.js @@ -48,7 +48,8 @@ angular.module('openproject.workPackages.directives') scope.$watch('groupSums.length', function() { // map columns to sums if the column data is a number setSums(); - scope.updateBackUrl(); + scope.$emit('queryStateChange'); + scope.$emit('workPackagesRefreshRequired'); }); } diff --git a/app/assets/javascripts/angular/work_packages/directives/work-package-total-sums-directive.js b/app/assets/javascripts/angular/work_packages/directives/work-package-total-sums-directive.js index c16b519e54..fe5e63de73 100644 --- a/app/assets/javascripts/angular/work_packages/directives/work-package-total-sums-directive.js +++ b/app/assets/javascripts/angular/work_packages/directives/work-package-total-sums-directive.js @@ -64,7 +64,7 @@ angular.module('openproject.workPackages.directives') scope.$watch(columnNames, function(columnNames, formerNames) { if (!angular.equals(columnNames, formerNames) && !totalSumsFetched()) { fetchTotalSums(); - scope.updateBackUrl(); + scope.$emit('queryStateChange'); } }, true); } diff --git a/app/assets/javascripts/angular/work_packages/directives/work-packages-table-directive.js b/app/assets/javascripts/angular/work_packages/directives/work-packages-table-directive.js index 048b54a9e0..b3aadc521f 100644 --- a/app/assets/javascripts/angular/work_packages/directives/work-packages-table-directive.js +++ b/app/assets/javascripts/angular/work_packages/directives/work-packages-table-directive.js @@ -50,9 +50,7 @@ angular.module('openproject.workPackages.directives') groupByColumn: '=', displaySums: '=', totalSums: '=', - groupSums: '=', - updateResults: '&', - updateBackUrl: '=' + groupSums: '=' }, link: function(scope, element, attributes) { var activeSelectionBorderIndex; diff --git a/app/assets/javascripts/angular/work_packages/tabs/work-package-relations-directive.js b/app/assets/javascripts/angular/work_packages/tabs/work-package-relations-directive.js index bf0f2b1c88..b1dfd8465c 100644 --- a/app/assets/javascripts/angular/work_packages/tabs/work-package-relations-directive.js +++ b/app/assets/javascripts/angular/work_packages/tabs/work-package-relations-directive.js @@ -72,7 +72,7 @@ angular.module('openproject.workPackages.tabs') }); scope.$watch('expand', function(newVal, oldVal) { - scope.stateClass = WorkPackagesHelper.collapseStateIcon(!newVal); + scope.stateClass = WorkPackagesHelper.collapseStateIcon(newVal); }); scope.toggleExpand = function() { diff --git a/config/locales/js-de.yml b/config/locales/js-de.yml index 6a09e2a779..a765d4c494 100644 --- a/config/locales/js-de.yml +++ b/config/locales/js-de.yml @@ -301,6 +301,8 @@ de: insert_columns: "Spalten hinzufügen ..." filters: "Filter" display_sums: "Summen anzeigen" + errors: + unretrievable_query: "Die URL enthält keine benutzerdefinierte Abfrage" tabs: overview: "Übersicht" activity: "Aktivität" diff --git a/config/locales/js-en.yml b/config/locales/js-en.yml index 342ccade77..28ef452350 100644 --- a/config/locales/js-en.yml +++ b/config/locales/js-en.yml @@ -304,6 +304,8 @@ en: insert_columns: "Insert columns ..." filters: "Filters" display_sums: "Display Sums" + errors: + unretrievable_query: "Unable to retrieve query from URL" tabs: overview: Overview activity: Activity diff --git a/karma/tests/controllers/work-packages-list-controller-test.js b/karma/tests/controllers/work-packages-list-controller-test.js index adf05d7516..db3fb83cc6 100644 --- a/karma/tests/controllers/work-packages-list-controller-test.js +++ b/karma/tests/controllers/work-packages-list-controller-test.js @@ -31,6 +31,7 @@ describe('WorkPackagesListController', function() { var scope, ctrl, win, testParams, testProjectService, testWorkPackageService, testQueryService, testPaginationService; + var testQueries; var buildController; beforeEach(module('openproject.api', 'openproject.workPackages.controllers', 'openproject.workPackages.services', 'ng-context-menu', 'btford.modal', 'openproject.services')); @@ -47,9 +48,24 @@ describe('WorkPackagesListController', function() { location: { pathname: "" } }; - var workPackageData = { + var defaultWorkPackagesData = { meta: { - } + query: { + _links: [] + }, + sums: [null] + }, + work_packages: [] + }; + var workPackagesDataByQueryId = { + meta: { + query: { + props: { id: 1 }, + _links: [] + }, + sums: [null] + }, + work_packages: [] }; var columnData = { }; @@ -58,6 +74,16 @@ describe('WorkPackagesListController', function() { var projectData = { embedded: { types: [] } }; var projectsData = [ projectData ]; + testQueries = { + '1': { + id: 1, + columns: ['type'] + }, + '2': { + id: 2, + columns: ['type'] + }, + }; testProjectService = { getProject: function(identifier) { @@ -74,15 +100,18 @@ describe('WorkPackagesListController', function() { testWorkPackageService = { getWorkPackages: function () { + return $timeout(function () { + return defaultWorkPackagesData; + }, 10); }, getWorkPackagesByQueryId: function (params) { return $timeout(function () { - return workPackageData; + return workPackagesDataByQueryId; }, 10); }, getWorkPackagesFromUrlQueryParams: function () { return $timeout(function () { - return workPackageData; + return defaultWorkPackagesData; }, 10); } }; @@ -93,8 +122,11 @@ describe('WorkPackagesListController', function() { } }; }, - initQuery: function () { + initQuery: function (id) { + var queryId = id || 1; + return testQueries[queryId]; }, + clearQuery: function() {}, getAvailableOptions: function() { return {}; }, @@ -130,11 +162,12 @@ describe('WorkPackagesListController', function() { setPage: function () { } }; + testAuthorisationService = { + initModelAuth: function(model, links) { + } + } - testParams = {}; - testState = {}; - - buildController = function() { + buildController = function(params, state, location) { scope.projectIdentifier = 'test'; ctrl = $controller("WorkPackagesListController", { @@ -142,19 +175,58 @@ describe('WorkPackagesListController', function() { $window: win, QueryService: testQueryService, PaginationService: testPaginationService, + ProjectService: testProjectService, WorkPackageService: testWorkPackageService, - $stateParams: testParams, - $state: testState, + $stateParams: params, + $state: state, + $location: location, latestTab: {} }); + + $timeout.flush(); }; })); - describe('initialisation', function() { + describe('initialisation of default query', function() { + beforeEach(function(){ + testParams = {}; + testState = { params: {} }; + testLocation = { + search: function() { + return {}; + } + } + + buildController(testParams, testState, testLocation); + }); + it('should initialise', function() { - buildController(); expect(scope.settingUpPage).to.be.defined; + expect(scope.operatorsAndLabelsByFilterType).to.be.defined; + expect(scope.disableFilters).to.eq(false); + expect(scope.disableNewWorkPackage).to.eq(true); + expect(scope.query.id).to.eq(testQueries['1'].id); + }); + }); + + describe('initialisation of query by id', function() { + beforeEach(function(){ + testParams = { }; + testState = { params: { + query_id: testQueries['2'].id + } }; + testLocation = { + search: function() { + return {}; + } + } + + buildController(testParams, testState, testLocation); + }); + + it('should initialise', function() { + expect(scope.query.id).to.eq(testQueries['2'].id); }); }); }); diff --git a/karma/tests/directives/components/table_pagination-test.js b/karma/tests/directives/components/table_pagination-test.js index 7d06918e94..eadc8047a7 100644 --- a/karma/tests/directives/components/table_pagination-test.js +++ b/karma/tests/directives/components/table_pagination-test.js @@ -34,7 +34,7 @@ describe('tablePagination Directive', function () { beforeEach(inject(function ($rootScope, $compile, _I18n_) { var html, I18n, t;; - html = ''; + html = ''; element = angular.element(html); rootScope = $rootScope; @@ -48,7 +48,7 @@ describe('tablePagination Directive', function () { })); describe('page ranges and links', function () { - beforeEach(function() { + beforeEach(function() { compile(); }); @@ -88,7 +88,7 @@ describe('tablePagination Directive', function () { }); describe('perPage options', function () { - beforeEach(function() { + beforeEach(function() { t = sinon.stub(I18n, 't'); t.withArgs('js.label_per_page').returns('Per page:'); compile(); @@ -104,24 +104,4 @@ describe('tablePagination Directive', function () { expect(perPageOptions.text()).to.include('Per page:'); }); }); - - describe('callback function', function() { - var updateResultsFn; - - beforeEach(function () { - updateResultsFn = sinon.spy(); - - scope.showUserSomething = updateResultsFn; - compile(); - }); - - it('should call the updateResults function on navigating page', function () { - var nextPage = element.find('.next_page'); - var previousPage = element.find('.previous_page'); - nextPage.click(); - previousPage.click(); - - expect(updateResultsFn).to.have.been.calledTwice; - }); - }); }); diff --git a/karma/tests/directives/work_packages/sort-header-directive-test.js b/karma/tests/directives/work_packages/sort-header-directive-test.js index 97f71606c3..341283e746 100644 --- a/karma/tests/directives/work_packages/sort-header-directive-test.js +++ b/karma/tests/directives/work_packages/sort-header-directive-test.js @@ -72,7 +72,7 @@ describe('sortHeader Directive', function() { beforeEach(function(){ query = new Query({ }); - query.setSortation(new Sortation([])); + query.setSortation('parent:desc'); scope.query = query; compile(); diff --git a/karma/tests/helpers/url-params-helper-test.js b/karma/tests/helpers/url-params-helper-test.js index 12756f4933..f852916153 100644 --- a/karma/tests/helpers/url-params-helper-test.js +++ b/karma/tests/helpers/url-params-helper-test.js @@ -55,4 +55,66 @@ describe('UrlParamsHelper', function() { expect(queryString).not.to.include('@'); }); }); + + describe('encodeQueryJsonParams', function(){ + var query; + + beforeEach(function() { + var filter = { + modelName: 'soße', + name: 'soße_id', + type: 'list model', + operator: '=', + values: ['knoblauch'] + }; + query = new Query({ + id: 1, + name: 'knoblauch soße', + projectId: 2, + displaySums: true, + columns: [{ name: 'type' }, { name: 'status' }, { name: 'soße' }], + groupBy: 'status', + sortCriteria: 'type:desc', + filters: [filter] + }, { rawFilters: true }); + }); + + it('should encode query to params JSON', function() { + var encodedJSON = UrlParamsHelper.encodeQueryJsonParams(query); + var expectedJSON = "{\"c\":[\"type\",\"status\",\"soße\"],\"s\":true,\"p\":2,\"g\":\"status\",\"t\":\"type:desc\",\"f\":[{\"n\":\"soße_id\",\"m\":\"soße\",\"o\":\"%3D\",\"t\":\"list model\",\"v\":[\"knoblauch\"]}]}"; + expect(encodedJSON).to.eq(expectedJSON); + }) + }); + + describe('decodeQueryFromJsonParams', function() { + var params; + var queryId; + + beforeEach(function() { + params = "{\"c\":[\"type\",\"status\",\"soße\"],\"s\":true,\"p\":2,\"g\":\"status\",\"t\":\"type:desc\",\"f\":[{\"n\":\"soße_id\",\"m\":\"soße\",\"o\":\"%3D\",\"t\":\"list model\",\"v\":[\"knoblauch\"]}]}"; + queryId = 2; + }); + + it('should decode query params to object', function() { + var decodedQueryParams = UrlParamsHelper.decodeQueryFromJsonParams(queryId, params); + + var expected = { + id: queryId, + projectId: 2, + displaySums: true, + columns: [{ name: 'type' }, { name: 'status' }, { name: 'soße' }], + groupBy: 'status', + sortCriteria: 'type:desc', + filters: [{ + modelName: 'soße', + name: 'soße_id', + type: 'list model', + operator: '=', + values: ['knoblauch'] + }] + }; + + expect(angular.equals(decodedQueryParams, expected)).to.be.true; + }); + }) }); diff --git a/karma/tests/models/query_test.js b/karma/tests/models/query_test.js index 77b6eedb2b..d99ca867a9 100644 --- a/karma/tests/models/query_test.js +++ b/karma/tests/models/query_test.js @@ -42,8 +42,9 @@ describe('Query', function() { }); it('should be a constructor function', function() { - expect(new Query()).to.exist; - expect(new Query()).to.be.an('object'); + var queryData = { id: 1 }; + expect(new Query(queryData)).to.exist; + expect(new Query(queryData)).to.be.an('object'); }); describe('adding filters', function(){ diff --git a/public/templates/components/selectable_title.html b/public/templates/components/selectable_title.html index 38ab3636f6..4ced06f46c 100644 --- a/public/templates/components/selectable_title.html +++ b/public/templates/components/selectable_title.html @@ -23,7 +23,7 @@
{{ group.name }}
  • diff --git a/public/templates/work_packages.list.html b/public/templates/work_packages.list.html index 0fa3359551..15a3bca3cc 100644 --- a/public/templates/work_packages.list.html +++ b/public/templates/work_packages.list.html @@ -2,7 +2,7 @@
    + transition-method="loadQuery">
    - +
    diff --git a/public/templates/work_packages/query_filters.html b/public/templates/work_packages/query_filters.html index 957ebf13c8..eba0ebf385 100644 --- a/public/templates/work_packages/query_filters.html +++ b/public/templates/work_packages/query_filters.html @@ -58,8 +58,7 @@ size="30" type="text" value="" - ng-disabled="isLoading" - execute-on-enter="updateResults()"/> + ng-disabled="isLoading"/>