diff --git a/app/assets/javascripts/angular/controllers/dialogs/save.js b/app/assets/javascripts/angular/controllers/dialogs/save.js index 7e0a8ab9e4..24f846f768 100644 --- a/app/assets/javascripts/angular/controllers/dialogs/save.js +++ b/app/assets/javascripts/angular/controllers/dialogs/save.js @@ -36,7 +36,15 @@ angular.module('openproject.workPackages.controllers') }); }]) -.controller('SaveModalController', ['saveModal', function(saveModal) { +.controller('SaveModalController', ['$scope', 'saveModal', 'QueryService', function($scope, saveModal, QueryService) { this.name = 'Save'; this.closeMe = saveModal.deactivate; + + $scope.saveQueryAs = function(name) { + QueryService.saveQueryAs(name) + .then(function(data){ + saveModal.deactivate(); + $scope.$emit('flashMessage', data.status); + }); + } }]); diff --git a/app/assets/javascripts/angular/controllers/work-packages-controller.js b/app/assets/javascripts/angular/controllers/work-packages-controller.js index eafc3fe262..43959c5673 100644 --- a/app/assets/javascripts/angular/controllers/work-packages-controller.js +++ b/app/assets/javascripts/angular/controllers/work-packages-controller.js @@ -109,11 +109,22 @@ angular.module('openproject.workPackages.controllers') $scope.showColumnsModal = columnsModal.activate; $scope.showExportModal = exportModal.activate; - $scope.showSaveModal = saveModal.activate; $scope.showSettingsModal = settingsModal.activate; $scope.showShareModal = shareModal.activate; $scope.showSortingModal = sortingModal.activate; + $scope.showSaveModal = function(saveAs){ + $scope.$emit('hideAllDropdowns'); + if( saveAs || $scope.query.isNew() ){ + saveModal.activate(); + } else { + QueryService.saveQuery() + .then(function(data){ + $scope.$emit('flashMessage', data.status); + }); + } + } + $scope.reloadQuery = function(queryId) { QueryService.resetQuery(); $scope.query_id = queryId; @@ -192,6 +203,13 @@ angular.module('openproject.workPackages.controllers') return WorkPackageLoadingHelper.withLoading($scope, callback, params, serviceErrorHandler); }; + // Note: I know we don't want watchers on the controller but I want all the toolbar directives to have restricted scopes. Thoughts welcome. + $scope.$watch('query.name', function(newValue, oldValue){ + if(newValue != oldValue){ + $scope.selectedTitle = newValue; + } + }); + setUrlParams($window.location); initialSetup(); }]); diff --git a/app/assets/javascripts/angular/directives/components/selectable-title-directive.js b/app/assets/javascripts/angular/directives/components/selectable-title-directive.js index 276977c17a..d4cb9913b7 100644 --- a/app/assets/javascripts/angular/directives/components/selectable-title-directive.js +++ b/app/assets/javascripts/angular/directives/components/selectable-title-directive.js @@ -47,6 +47,7 @@ angular.module('openproject.uiComponents') scope.reload = function(modelId, newTitle) { scope.selectedTitle = newTitle; scope.reloadMethod(modelId); + scope.$emit('hideAllDropdowns'); } scope.filterModels = function(filterBy) { diff --git a/app/assets/javascripts/angular/directives/components/toolbar.js b/app/assets/javascripts/angular/directives/components/toolbar.js index 2ad49d7933..6975de087d 100644 --- a/app/assets/javascripts/angular/directives/components/toolbar.js +++ b/app/assets/javascripts/angular/directives/components/toolbar.js @@ -34,7 +34,6 @@ angular.module('openproject.uiComponents') restrict: 'EA', scope: {}, link: function(scope, element, attributes) { - // TODO: implement me } }; }); diff --git a/app/assets/javascripts/angular/directives/components/with-dropdown.js b/app/assets/javascripts/angular/directives/components/with-dropdown.js index 0de8cdeb6e..3ba5ae18f9 100644 --- a/app/assets/javascripts/angular/directives/components/with-dropdown.js +++ b/app/assets/javascripts/angular/directives/components/with-dropdown.js @@ -29,11 +29,7 @@ // TODO move to UI components angular.module('openproject.uiComponents') - .directive('withDropdown', function () { - - function hideAllDropdowns() { - jQuery('.dropdown').hide(); - } + .directive('withDropdown', ['$rootScope', function ($rootScope) { function position(dropdown, trigger) { var hOffset = 0, @@ -67,6 +63,10 @@ angular.module('openproject.uiComponents') dropdownId: '@' }, link: function (scope, element, attributes) { + $rootScope.$on('hideAllDropdowns', function(event){ + jQuery('.dropdown').hide(); + }); + element.on('click', function () { var trigger = jQuery(this), @@ -74,11 +74,12 @@ angular.module('openproject.uiComponents') event.preventDefault(); event.stopPropagation(); - hideAllDropdowns(); + + scope.$emit('hideAllDropdowns'); dropdown.show(); position(dropdown, trigger); }); } }; - }); + }]); diff --git a/app/assets/javascripts/angular/directives/work_packages/query-filter-directive.js b/app/assets/javascripts/angular/directives/work_packages/query-filter-directive.js index 9591f57fa8..ac623f6b47 100644 --- a/app/assets/javascripts/angular/directives/work_packages/query-filter-directive.js +++ b/app/assets/javascripts/angular/directives/work_packages/query-filter-directive.js @@ -63,7 +63,6 @@ angular.module('openproject.workPackages.directives') scope.$watch('filter', function(filter, oldFilter) { if (filter !== oldFilter) { if (filter.isConfigured() || valueReset(filter, oldFilter)) { - scope.query.hasChanged(); PaginationService.resetPage(); applyFilters(); diff --git a/app/assets/javascripts/angular/helpers/components/path-helper.js b/app/assets/javascripts/angular/helpers/components/path-helper.js index fc028c71b0..345ca40d19 100644 --- a/app/assets/javascripts/angular/helpers/components/path-helper.js +++ b/app/assets/javascripts/angular/helpers/components/path-helper.js @@ -55,6 +55,9 @@ angular.module('openproject.helpers') projectPath: function(projectIdentifier) { return PathHelper.projectsPath() + '/' + projectIdentifier; }, + queryPath: function(queryIdentifier) { + return '/queries/' + queryIdentifier; + }, timeEntriesPath: function(projectIdentifier, workPackageIdentifier) { var path = '/time_entries/'; @@ -116,6 +119,12 @@ angular.module('openproject.helpers') apiProjectSubProjectsPath: function(projectIdentifier) { return PathHelper.apiV3ProjectPath(projectIdentifier) + PathHelper.subProjectsPath(); }, + apiProjectQueriesPath: function(projectIdentifier) { + return PathHelper.apiV3ProjectPath(projectIdentifier) + '/queries' + }, + apiProjectQueryPath: function(projectIdentifier, queryIdentifier) { + return PathHelper.apiV3ProjectPath(projectIdentifier) + PathHelper.queryPath(queryIdentifier) + }, apiGroupedQueriesPath: function() { return PathHelper.apiPrefixV3 + '/queries/grouped'; }, diff --git a/app/assets/javascripts/angular/models/query.js b/app/assets/javascripts/angular/models/query.js index c70c0f7ce9..0ec8506763 100644 --- a/app/assets/javascripts/angular/models/query.js +++ b/app/assets/javascripts/angular/models/query.js @@ -50,6 +50,22 @@ angular.module('openproject.models') toParams: function() { return angular.extend.apply(this, [ { + 'f[]': this.getFilterNames(this.getActiveConfiguredFilters()), + 'c[]': this.getParamColumns(), + 'group_by': this.groupBy, + 'sort': this.sortation.encode(), + 'display_sums': this.displaySums, + 'name': this.name + }].concat(this.getActiveConfiguredFilters().map(function(filter) { + return filter.toParams(); + })) + ); + }, + + toUpdateParams: function() { + return angular.extend.apply(this, [ + { + 'id': this.id, 'f[]': this.getFilterNames(this.getActiveConfiguredFilters()), 'c[]': this.getParamColumns(), 'group_by': this.groupBy, @@ -61,6 +77,12 @@ angular.module('openproject.models') ); }, + save: function(data){ + // Note: query has already been updated, only the id needs to be set + this.id = data.id; + return this; + }, + serialiseForAngular: function(){ var params = this.toParams(); var serialised = ''; @@ -215,14 +237,16 @@ angular.module('openproject.models') }); }, - // Note: If we pass an id for the query then any changes to filters are ignored by the server and it - // just uses the queries filters. Therefor we have to set it to null. - hasChanged: function(){ - this.id = null; + isNew: function(){ + return !this.id; }, setSortation: function(sortation){ this.sortation = sortation; + }, + + setName: function(name) { + this.name = name; } }; diff --git a/app/assets/javascripts/angular/services/query-service.js b/app/assets/javascripts/angular/services/query-service.js index 1bbd286bb4..83b6f3f378 100644 --- a/app/assets/javascripts/angular/services/query-service.js +++ b/app/assets/javascripts/angular/services/query-service.js @@ -28,8 +28,8 @@ angular.module('openproject.services') -.service('QueryService', ['Query', 'Sortation', '$http', '$location', 'PathHelper', '$q', 'AVAILABLE_WORK_PACKAGE_FILTERS', 'StatusService', 'TypeService', 'PriorityService', 'UserService', 'VersionService', 'RoleService', 'GroupService', 'ProjectService', - function(Query, Sortation, $http, $location, PathHelper, $q, AVAILABLE_WORK_PACKAGE_FILTERS, StatusService, TypeService, PriorityService, UserService, VersionService, RoleService, GroupService, ProjectService) { +.service('QueryService', ['Query', 'Sortation', '$http', '$location', 'PathHelper', '$q', 'AVAILABLE_WORK_PACKAGE_FILTERS', 'StatusService', 'TypeService', 'PriorityService', 'UserService', 'VersionService', 'RoleService', 'GroupService', 'ProjectService', 'I18n', + function(Query, Sortation, $http, $location, PathHelper, $q, AVAILABLE_WORK_PACKAGE_FILTERS, StatusService, TypeService, PriorityService, UserService, VersionService, RoleService, GroupService, ProjectService, I18n) { var query; @@ -182,15 +182,37 @@ angular.module('openproject.services') return availableFilters[projectIdentifier]; }, - doQuery: function(url, params) { + saveQuery: function() { + var url = PathHelper.apiProjectQueryPath(query.project_id, query.id); + return QueryService.doQuery(url, query.toUpdateParams(), 'PUT', function(response){ + return angular.extend(response.data, { status: { text: I18n.t('js.notice_successful_update') }} ); + }); + }, + + saveQueryAs: function(name) { + query.setName(name); + var url = PathHelper.apiProjectQueriesPath(query.project_id); + return QueryService.doQuery(url, query.toParams(), 'POST', function(response){ + query.save(response.data); + return angular.extend(response.data, { status: { text: I18n.t('js.notice_successful_create') }} ); + }); + }, + + doQuery: function(url, params, method, success, failure) { + method = method || 'GET'; + success = success || function(response){ + return response.data; + }; + failure = failure || function(response){ + return angular.extend(response.data, { status: { text: I18n.t('js.notice_bad_request'), isError: true }} ); + } + return $http({ - method: 'GET', + method: method, url: url, params: params, headers: {'Content-Type': 'application/x-www-form-urlencoded'} - }).then(function(response){ - return response.data; - }); + }).then(success, failure); } }; diff --git a/app/controllers/api/v3/queries_controller.rb b/app/controllers/api/v3/queries_controller.rb index 71385a20e8..1a34b76edd 100644 --- a/app/controllers/api/v3/queries_controller.rb +++ b/app/controllers/api/v3/queries_controller.rb @@ -39,6 +39,8 @@ module Api::V3 include ExtendedHTTP before_filter :find_optional_project + before_filter :setup_query_for_create, only: [:create] + before_filter :setup_query_for_update, only: [:update] before_filter :setup_query, only: [:available_columns, :custom_field_filters] def available_columns @@ -71,12 +73,57 @@ module Api::V3 end end + def create + if @query.save + respond_to do |format| + format.api + end + else + render json: @query.errors.to_json, status: 422 + end + end + + def update + if @query.save + respond_to do |format| + format.api + end + else + render json: @query.errors.to_json, status: 422 + end + end + private def setup_query @query = retrieve_query end + # Note: Not dry - lifted straight from old queries controller + def setup_query_for_create + @query = Query.new params[:query] ? permitted_params.query : nil + @query.project = @project unless params[:query_is_for_all] + prepare_query @query + @query.user = User.current + end + + def setup_query_for_update + @query = Query.find(params[:id]) + prepare_query(@query) + end + + # Note: Not dry - lifted straight from old queries controller + def prepare_query(query) + @query.is_public = false unless User.current.allowed_to?(:manage_public_queries, @project) || User.current.admin? + view_context.add_filter_from_params if params[:fields] || params[:f] + @query.group_by ||= params[:group_by] + @query.project = nil if params[:query_is_for_all] + @query.display_sums ||= params[:display_sums].present? + @query.column_names = params[:c] if params[:c] + @query.column_names = nil if params[:default_columns] + @query.name = params[:name] if params[:name] + end + def visible_queries unless @visible_queries # User can see public queries and his own queries diff --git a/app/views/api/v3/queries/create.api.rabl b/app/views/api/v3/queries/create.api.rabl new file mode 100644 index 0000000000..67f21d3d6b --- /dev/null +++ b/app/views/api/v3/queries/create.api.rabl @@ -0,0 +1,4 @@ + +object @query => :query + +attributes :id, :name diff --git a/app/views/api/v3/queries/update.api.rabl b/app/views/api/v3/queries/update.api.rabl new file mode 100644 index 0000000000..67f21d3d6b --- /dev/null +++ b/app/views/api/v3/queries/update.api.rabl @@ -0,0 +1,4 @@ + +object @query => :query + +attributes :id, :name diff --git a/app/views/work_packages/index.html.erb b/app/views/work_packages/index.html.erb index 143b04c913..48ffd6521a 100644 --- a/app/views/work_packages/index.html.erb +++ b/app/views/work_packages/index.html.erb @@ -78,7 +78,7 @@ end
  • Display sums
  • Save
  • -
  • Save as
  • +
  • Save as
  • Export
  • Share
  • Page settings
  • diff --git a/config/locales/js-de.yml b/config/locales/js-de.yml index b2287df36a..e0002fb28f 100644 --- a/config/locales/js-de.yml +++ b/config/locales/js-de.yml @@ -205,3 +205,7 @@ de: group_by: "Gruppiere Ergebnisse nach" filters: "Filter" display_sums: "Summen anzeigen" + notice_successful_create: "Erfolgreich angelegt" + notice_successful_delete: "Erfolgreich gelöscht." + notice_successful_update: "Erfolgreich aktualisiert." + notice_bad_request: "Fehlerhafte Anfrage." diff --git a/config/locales/js-en.yml b/config/locales/js-en.yml index 0f591e39cd..6e030e6ef8 100644 --- a/config/locales/js-en.yml +++ b/config/locales/js-en.yml @@ -208,3 +208,7 @@ en: group_by: "Group results by" filters: "Filters" display_sums: "Display Sums" + notice_successful_create: "Successful creation." + notice_successful_delete: "Successful deletion." + notice_successful_update: "Successful update." + notice_bad_request: "Bad Request." diff --git a/config/routes.rb b/config/routes.rb index 35f006a0d8..821f5da1f8 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -118,7 +118,7 @@ OpenProject::Application.routes.draw do get :column_data, on: :collection get :column_sums, on: :collection end - resources :queries, only: [:show] do + resources :queries, only: [:show, :create, :update] do get :available_columns, on: :collection get :custom_field_filters, on: :collection get :grouped, on: :collection @@ -128,7 +128,7 @@ OpenProject::Application.routes.draw do resources :work_packages, only: [:index] do get :column_sums, on: :collection end - resources :queries, only: [:show] do + resources :queries, only: [:show, :create, :update] do get :available_columns, on: :collection get :custom_field_filters, on: :collection get :grouped, on: :collection diff --git a/lib/redmine.rb b/lib/redmine.rb index 3b98f396a8..e2ce7767d9 100644 --- a/lib/redmine.rb +++ b/lib/redmine.rb @@ -106,7 +106,7 @@ Redmine::AccessControl.map do |map| :auto_complete => [:issues], :versions => [:index, :show, :status_by], :journals => [:index, :diff], - :queries => [:index, :available_columns, :custom_field_filters, :grouped], + :queries => [:index, :create, :update, :available_columns, :custom_field_filters, :grouped], :work_packages => [:show, :index], :'work_packages/reports' => [:report, :report_details], :planning_elements => [:index, :all, :show, :recycle_bin], diff --git a/public/templates/work_packages/modals/save.html b/public/templates/work_packages/modals/save.html index 54ea392fd3..484d1f3421 100644 --- a/public/templates/work_packages/modals/save.html +++ b/public/templates/work_packages/modals/save.html @@ -4,5 +4,14 @@

    Save

    +
    + + +
    +
    + + +
    +