Merge pull request #1272 from opf/feature/ng-toolbar-save-query

Feature/ng toolbar save query
pull/1282/head
Alex Coles 11 years ago
commit 106f99513c
  1. 10
      app/assets/javascripts/angular/controllers/dialogs/save.js
  2. 20
      app/assets/javascripts/angular/controllers/work-packages-controller.js
  3. 1
      app/assets/javascripts/angular/directives/components/selectable-title-directive.js
  4. 1
      app/assets/javascripts/angular/directives/components/toolbar.js
  5. 15
      app/assets/javascripts/angular/directives/components/with-dropdown.js
  6. 1
      app/assets/javascripts/angular/directives/work_packages/query-filter-directive.js
  7. 9
      app/assets/javascripts/angular/helpers/components/path-helper.js
  8. 32
      app/assets/javascripts/angular/models/query.js
  9. 36
      app/assets/javascripts/angular/services/query-service.js
  10. 47
      app/controllers/api/v3/queries_controller.rb
  11. 4
      app/views/api/v3/queries/create.api.rabl
  12. 4
      app/views/api/v3/queries/update.api.rabl
  13. 2
      app/views/work_packages/index.html.erb
  14. 4
      config/locales/js-de.yml
  15. 4
      config/locales/js-en.yml
  16. 4
      config/routes.rb
  17. 2
      lib/redmine.rb
  18. 9
      public/templates/work_packages/modals/save.html

@ -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.name = 'Save';
this.closeMe = saveModal.deactivate; this.closeMe = saveModal.deactivate;
$scope.saveQueryAs = function(name) {
QueryService.saveQueryAs(name)
.then(function(data){
saveModal.deactivate();
$scope.$emit('flashMessage', data.status);
});
}
}]); }]);

@ -109,11 +109,22 @@ angular.module('openproject.workPackages.controllers')
$scope.showColumnsModal = columnsModal.activate; $scope.showColumnsModal = columnsModal.activate;
$scope.showExportModal = exportModal.activate; $scope.showExportModal = exportModal.activate;
$scope.showSaveModal = saveModal.activate;
$scope.showSettingsModal = settingsModal.activate; $scope.showSettingsModal = settingsModal.activate;
$scope.showShareModal = shareModal.activate; $scope.showShareModal = shareModal.activate;
$scope.showSortingModal = sortingModal.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) { $scope.reloadQuery = function(queryId) {
QueryService.resetQuery(); QueryService.resetQuery();
$scope.query_id = queryId; $scope.query_id = queryId;
@ -192,6 +203,13 @@ angular.module('openproject.workPackages.controllers')
return WorkPackageLoadingHelper.withLoading($scope, callback, params, serviceErrorHandler); 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); setUrlParams($window.location);
initialSetup(); initialSetup();
}]); }]);

@ -47,6 +47,7 @@ angular.module('openproject.uiComponents')
scope.reload = function(modelId, newTitle) { scope.reload = function(modelId, newTitle) {
scope.selectedTitle = newTitle; scope.selectedTitle = newTitle;
scope.reloadMethod(modelId); scope.reloadMethod(modelId);
scope.$emit('hideAllDropdowns');
} }
scope.filterModels = function(filterBy) { scope.filterModels = function(filterBy) {

@ -34,7 +34,6 @@ angular.module('openproject.uiComponents')
restrict: 'EA', restrict: 'EA',
scope: {}, scope: {},
link: function(scope, element, attributes) { link: function(scope, element, attributes) {
// TODO: implement me
} }
}; };
}); });

@ -29,11 +29,7 @@
// TODO move to UI components // TODO move to UI components
angular.module('openproject.uiComponents') angular.module('openproject.uiComponents')
.directive('withDropdown', function () { .directive('withDropdown', ['$rootScope', function ($rootScope) {
function hideAllDropdowns() {
jQuery('.dropdown').hide();
}
function position(dropdown, trigger) { function position(dropdown, trigger) {
var hOffset = 0, var hOffset = 0,
@ -67,6 +63,10 @@ angular.module('openproject.uiComponents')
dropdownId: '@' dropdownId: '@'
}, },
link: function (scope, element, attributes) { link: function (scope, element, attributes) {
$rootScope.$on('hideAllDropdowns', function(event){
jQuery('.dropdown').hide();
});
element.on('click', function () { element.on('click', function () {
var trigger = jQuery(this), var trigger = jQuery(this),
@ -74,11 +74,12 @@ angular.module('openproject.uiComponents')
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
hideAllDropdowns();
scope.$emit('hideAllDropdowns');
dropdown.show(); dropdown.show();
position(dropdown, trigger); position(dropdown, trigger);
}); });
} }
}; };
}); }]);

@ -63,7 +63,6 @@ angular.module('openproject.workPackages.directives')
scope.$watch('filter', function(filter, oldFilter) { scope.$watch('filter', function(filter, oldFilter) {
if (filter !== oldFilter) { if (filter !== oldFilter) {
if (filter.isConfigured() || valueReset(filter, oldFilter)) { if (filter.isConfigured() || valueReset(filter, oldFilter)) {
scope.query.hasChanged();
PaginationService.resetPage(); PaginationService.resetPage();
applyFilters(); applyFilters();

@ -55,6 +55,9 @@ angular.module('openproject.helpers')
projectPath: function(projectIdentifier) { projectPath: function(projectIdentifier) {
return PathHelper.projectsPath() + '/' + projectIdentifier; return PathHelper.projectsPath() + '/' + projectIdentifier;
}, },
queryPath: function(queryIdentifier) {
return '/queries/' + queryIdentifier;
},
timeEntriesPath: function(projectIdentifier, workPackageIdentifier) { timeEntriesPath: function(projectIdentifier, workPackageIdentifier) {
var path = '/time_entries/'; var path = '/time_entries/';
@ -116,6 +119,12 @@ angular.module('openproject.helpers')
apiProjectSubProjectsPath: function(projectIdentifier) { apiProjectSubProjectsPath: function(projectIdentifier) {
return PathHelper.apiV3ProjectPath(projectIdentifier) + PathHelper.subProjectsPath(); 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() { apiGroupedQueriesPath: function() {
return PathHelper.apiPrefixV3 + '/queries/grouped'; return PathHelper.apiPrefixV3 + '/queries/grouped';
}, },

@ -50,6 +50,22 @@ angular.module('openproject.models')
toParams: function() { toParams: function() {
return angular.extend.apply(this, [ 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()), 'f[]': this.getFilterNames(this.getActiveConfiguredFilters()),
'c[]': this.getParamColumns(), 'c[]': this.getParamColumns(),
'group_by': this.groupBy, '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(){ serialiseForAngular: function(){
var params = this.toParams(); var params = this.toParams();
var serialised = ''; 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 isNew: function(){
// just uses the queries filters. Therefor we have to set it to null. return !this.id;
hasChanged: function(){
this.id = null;
}, },
setSortation: function(sortation){ setSortation: function(sortation){
this.sortation = sortation; this.sortation = sortation;
},
setName: function(name) {
this.name = name;
} }
}; };

@ -28,8 +28,8 @@
angular.module('openproject.services') angular.module('openproject.services')
.service('QueryService', ['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) { function(Query, Sortation, $http, $location, PathHelper, $q, AVAILABLE_WORK_PACKAGE_FILTERS, StatusService, TypeService, PriorityService, UserService, VersionService, RoleService, GroupService, ProjectService, I18n) {
var query; var query;
@ -182,15 +182,37 @@ angular.module('openproject.services')
return availableFilters[projectIdentifier]; 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({ return $http({
method: 'GET', method: method,
url: url, url: url,
params: params, params: params,
headers: {'Content-Type': 'application/x-www-form-urlencoded'} headers: {'Content-Type': 'application/x-www-form-urlencoded'}
}).then(function(response){ }).then(success, failure);
return response.data;
});
} }
}; };

@ -39,6 +39,8 @@ module Api::V3
include ExtendedHTTP include ExtendedHTTP
before_filter :find_optional_project 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] before_filter :setup_query, only: [:available_columns, :custom_field_filters]
def available_columns def available_columns
@ -71,12 +73,57 @@ module Api::V3
end end
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 private
def setup_query def setup_query
@query = retrieve_query @query = retrieve_query
end 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 def visible_queries
unless @visible_queries unless @visible_queries
# User can see public queries and his own queries # User can see public queries and his own queries

@ -0,0 +1,4 @@
object @query => :query
attributes :id, :name

@ -0,0 +1,4 @@
object @query => :query
attributes :id, :name

@ -78,7 +78,7 @@ end
<li><a href>Display sums</a></li> <li><a href>Display sums</a></li>
<li class="dropdown-divider"></li> <li class="dropdown-divider"></li>
<li><a href ng-click="showSaveModal()">Save</a></li> <li><a href ng-click="showSaveModal()">Save</a></li>
<li><a href ng-click="showSaveModal()">Save as</a></li> <li><a href ng-click="showSaveModal(true)">Save as</a></li>
<li><a href ng-click="showExportModal()">Export</a></li> <li><a href ng-click="showExportModal()">Export</a></li>
<li><a href ng-click="showShareModal()">Share</a></li> <li><a href ng-click="showShareModal()">Share</a></li>
<li><a href ng-click="showSettingsModal()">Page settings</a></li> <li><a href ng-click="showSettingsModal()">Page settings</a></li>

@ -205,3 +205,7 @@ de:
group_by: "Gruppiere Ergebnisse nach" group_by: "Gruppiere Ergebnisse nach"
filters: "Filter" filters: "Filter"
display_sums: "Summen anzeigen" 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."

@ -208,3 +208,7 @@ en:
group_by: "Group results by" group_by: "Group results by"
filters: "Filters" filters: "Filters"
display_sums: "Display Sums" display_sums: "Display Sums"
notice_successful_create: "Successful creation."
notice_successful_delete: "Successful deletion."
notice_successful_update: "Successful update."
notice_bad_request: "Bad Request."

@ -118,7 +118,7 @@ OpenProject::Application.routes.draw do
get :column_data, on: :collection get :column_data, on: :collection
get :column_sums, on: :collection get :column_sums, on: :collection
end end
resources :queries, only: [:show] do resources :queries, only: [:show, :create, :update] do
get :available_columns, on: :collection get :available_columns, on: :collection
get :custom_field_filters, on: :collection get :custom_field_filters, on: :collection
get :grouped, on: :collection get :grouped, on: :collection
@ -128,7 +128,7 @@ OpenProject::Application.routes.draw do
resources :work_packages, only: [:index] do resources :work_packages, only: [:index] do
get :column_sums, on: :collection get :column_sums, on: :collection
end end
resources :queries, only: [:show] do resources :queries, only: [:show, :create, :update] do
get :available_columns, on: :collection get :available_columns, on: :collection
get :custom_field_filters, on: :collection get :custom_field_filters, on: :collection
get :grouped, on: :collection get :grouped, on: :collection

@ -106,7 +106,7 @@ Redmine::AccessControl.map do |map|
:auto_complete => [:issues], :auto_complete => [:issues],
:versions => [:index, :show, :status_by], :versions => [:index, :show, :status_by],
:journals => [:index, :diff], :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 => [:show, :index],
:'work_packages/reports' => [:report, :report_details], :'work_packages/reports' => [:report, :report_details],
:planning_elements => [:index, :all, :show, :recycle_bin], :planning_elements => [:index, :all, :show, :recycle_bin],

@ -4,5 +4,14 @@
<h1>Save</h1> <h1>Save</h1>
<div>
<label for="name">Name</label>
<input type="text" name="query_name" ng-model="queryName"></input>
</div>
<div>
<button ng-click="modal.closeMe()">Cancel</button>
<button ng-click="saveQueryAs(queryName)">Save</button>
</div>
</div> </div>
</div> </div>

Loading…
Cancel
Save