Merge branch 'feature/ui-component-table-widgets-merged' into feature/ui-components-test-architecture-bower

pull/1035/head
Alex Coles 11 years ago
commit 67ee568721
  1. 24
      app/assets/javascripts/angular/directives/components/progress-bar-directive.js
  2. 52
      app/assets/javascripts/angular/directives/components/work-package-column-directive.js
  3. 1
      app/assets/javascripts/angular/directives/timelines/timeline-column-name-directive.js
  4. 18
      app/assets/javascripts/angular/directives/work_packages/query-filter-directive.js
  5. 15
      app/assets/javascripts/angular/directives/work_packages/sort-header-directive.js
  6. 73
      app/assets/javascripts/angular/directives/work_packages/work-package-column-directive.js
  7. 4
      app/assets/javascripts/angular/directives/work_packages/work-package-total-sums-directive.js
  8. 5
      app/assets/javascripts/angular/directives/work_packages/work-packages-table-directive.js
  9. 14
      app/assets/javascripts/angular/helpers/components/custom-field-helper.js
  10. 18
      app/assets/javascripts/angular/helpers/components/function-decorators.js
  11. 12
      app/assets/javascripts/angular/helpers/components/path-helper.js
  12. 41
      app/assets/javascripts/angular/helpers/components/work-packages-helper.js
  13. 2
      app/assets/javascripts/angular/helpers/filter-query-string-builder.js
  14. 4
      app/assets/javascripts/angular/helpers/svg-helper.js
  15. 2
      app/assets/javascripts/angular/models/timelines/mixins/constants.js
  16. 2
      app/assets/javascripts/angular/models/timelines/mixins/ui.js
  17. 2
      app/assets/javascripts/angular/models/timelines/project_type.js
  18. 13
      app/assets/javascripts/angular/openproject-app.js
  19. 2
      app/assets/javascripts/angular/services/timeline-loader-service.js
  20. 44
      app/assets/javascripts/angular/services/user-service.js
  21. 1
      app/assets/stylesheets/default/main.css.erb
  22. 16
      app/controllers/boards_controller.rb
  23. 8
      app/controllers/messages_controller.rb
  24. 51
      app/controllers/work_packages_controller.rb
  25. 4
      app/models/board.rb
  26. 10
      app/models/message.rb
  27. 2
      app/views/messages/show.html.erb
  28. 2
      app/views/news/index.html.erb
  29. 4
      app/views/news/show.html.erb
  30. 38
      db/migrate/20130813062523_fix_customizable_journal_value_column.rb
  31. 34
      db/migrate/20140311120609_add_sticked_on_field_to_messages.rb
  32. 2
      doc/CHANGELOG.md
  33. 39
      features/messages/message.feature
  34. 2
      features/planning_elements/planning_element_management.feature.disabled
  35. 2
      features/search/pagination.feature
  36. 3
      features/step_definitions/common_steps.rb
  37. 2
      features/work_packages/index_move_columns.feature
  38. 2
      features/work_packages/index_sums.feature
  39. 2
      features/work_packages/work_packages_new.feature
  40. 11
      public/templates/components/progress_bar.html
  41. 6
      public/templates/components/work_package_column.html
  42. 12
      public/templates/work_packages/work_package_column.html
  43. 18
      public/templates/work_packages/work_packages_table.html
  44. 59
      spec/controllers/boards_controller_spec.rb
  45. 22
      spec/models/work_package/work_package_custom_fields_spec.rb

@ -0,0 +1,24 @@
// TODO move to UI components
angular.module('openproject.uiComponents')
.directive('progressBar', [function() {
return {
restrict: 'EA',
replace: true,
scope: {
progress: '=',
width: '@',
legend: '@'
},
templateUrl: '/templates/components/progress_bar.html',
link: function(scope) {
// apply defaults
scope.progress = scope.progress || 0;
scope.width = scope.width || '100px';
scope.legend = scope.legend || '';
scope.scaleLength = 100;
scope.progress = Math.round(scope.progress);
}
};
}]);

@ -1,52 +0,0 @@
// TODO move to UI components
angular.module('openproject.uiComponents')
.directive('workPackageColumn', ['PathHelper', 'WorkPackagesHelper', function(PathHelper, WorkPackagesHelper){
return {
restrict: 'EA',
replace: true,
scope: {
workPackage: '=',
column: '='
},
templateUrl: '/templates/components/work_package_column.html',
link: function(scope, element, attributes) {
var defaultText = '';
var defaultType = 'text';
// Set text to be displayed
scope.$watch('workPackage', updateColumnData, true);
function updateColumnData() {
scope.displayText = WorkPackagesHelper.getColumnValue(scope.workPackage, scope.column) || defaultText;
scope.displayType = defaultType;
// Example of how we can look to the provided meta data to format the column
// This relies on the meta being sent from the server
if (scope.column.meta_data.link.display) {
scope.displayType = 'link';
scope.url = getLinkFor(scope.column.meta_data.link);
}
}
function getLinkFor(link_meta){
switch (link_meta.model_type){
case 'work_package':
url = PathHelper.workPackagePath(scope.workPackage.id);
break;
case 'user':
if (scope.workPackage[scope.column.name]) url = PathHelper.userPath(scope.workPackage[scope.column.name].id);
break;
case 'project':
if (scope.workPackage.project) url = PathHelper.projectPath(scope.workPackage.project.identifier);
break;
default:
url = "";
};
return url;
}
}
};
}]);

@ -19,7 +19,6 @@ angular.module('openproject.timelines.directives')
} else {
element.html(I18n.t(scope.localePrefix + '.' + scope.columnName));
}
}
};
}]);

@ -1,6 +1,6 @@
angular.module('openproject.workPackages.directives')
.directive('queryFilter', ['WorkPackagesTableHelper', 'WorkPackageService', '$timeout', function(WorkPackagesTableHelper, WorkPackageService, $timeout) {
.directive('queryFilter', ['WorkPackagesTableHelper', 'WorkPackageService', 'FunctionDecorators', function(WorkPackagesTableHelper, WorkPackageService, FunctionDecorators) {
return {
restrict: 'A',
@ -24,23 +24,9 @@ angular.module('openproject.workPackages.directives')
}
}, true);
var currentRun;
// TODO move to some application helper
function withDelay(delay, callback, params){
$timeout.cancel(currentRun);
currentRun = $timeout(function() {
return callback.apply(this, params);
}, delay);
return currentRun;
}
function applyFiltersWithDelay() {
return withDelay(800, scope.updateResults);
return FunctionDecorators.withDelay(800, scope.updateResults);
}
}
};
}]);

@ -7,16 +7,12 @@ angular.module('openproject.workPackages.directives')
templateUrl: '/templates/work_packages/sort_header.html',
scope: {
query: '=',
column: '=',
headerName: '=',
headerTitle: '=',
sortable: '=',
updateResults: '&'
},
link: function(scope, element, attributes) {
scope.$watch('query.sortation', function(oldValue, newValue) {
if (newValue !== oldValue) {
scope.updateResults();
}
});
scope.performSort = function(){
targetSortation = scope.query.sortation.getTargetSortationOfHeader(scope.headerName);
scope.query.setSortation(targetSortation);
@ -32,11 +28,8 @@ angular.module('openproject.workPackages.directives')
} else {
scope.fullTitle = (I18n.t('js.label_sort_by') + ' \"' + scope.headerTitle + '\"');
}
}
};
scope.headerName = attributes['headerName'];
scope.headerTitle = attributes['headerTitle'];
scope.sortable = attributes['sortable'];
scope.currentSortDirection = scope.query.sortation.getDisplayedSortDirectionOfHeader(scope.headerName);
scope.setFullTitle();
}

@ -0,0 +1,73 @@
// TODO move to UI components
angular.module('openproject.workPackages.directives')
.directive('workPackageColumn', ['PathHelper', 'WorkPackagesHelper', 'UserService', function(PathHelper, WorkPackagesHelper, UserService){
return {
restrict: 'EA',
replace: true,
scope: {
workPackage: '=',
column: '=',
displayType: '@'
},
templateUrl: '/templates/work_packages/work_package_column.html',
link: function(scope, element, attributes) {
scope.displayType = scope.displayType || 'text';
// custom display types
if (scope.column.name === 'done_ratio') {
scope.displayType = 'progress_bar';
}
// Set text to be displayed
scope.$watch('workPackage', setColumnData, true);
function setColumnData() {
// retrieve column value from work package
scope.displayText = WorkPackagesHelper.getFormattedColumnValue(scope.workPackage, scope.column) || '';
if (scope.column.meta_data.data_type === 'user') loadUserName();
// Example of how we can look to the provided meta data to format the column
// This relies on the meta being sent from the server
if (scope.column.meta_data.link.display) {
scope.displayType = 'link';
scope.url = getLinkFor(scope.column.meta_data.link);
}
}
function loadUserName() {
var userId = scope.displayText;
if(userId) {
scope.user = UserService.registerUserId(userId);
scope.$watch('user.name', function(userName) {
// triggered when user data is loaded
// TODO replace watcher as soon as data is loaded via a promise chain
scope.displayText = userName;
});
}
}
function getLinkFor(link_meta){
if (link_meta.model_type === 'work_package') {
return PathHelper.workPackagePath(scope.workPackage.id);
} else if (scope.workPackage[scope.column.name]) {
switch (link_meta.model_type) {
case 'user':
return PathHelper.userPath(scope.workPackage[scope.column.name].id);
case 'version':
return PathHelper.versionPath(scope.workPackage[scope.column.name].id);
case 'project':
return PathHelper.projectPath(scope.workPackage.project.identifier);
default:
return '';
}
}
}
}
};
}]);

@ -15,9 +15,9 @@ angular.module('openproject.workPackages.directives')
});
}
scope.$watch('columns.length', function() {
scope.$watch('columns.length', function(length, formerLength) {
// map columns to sums if the column data is a number
fetchSums();
if(length >= formerLength) fetchSums();
});
}
};

@ -32,6 +32,11 @@ angular.module('openproject.workPackages.directives')
});
};
scope.$watch('query.sortation.sortElements', function(oldValue, newValue) {
if (newValue !== oldValue) {
scope.updateResults();
}
});
}
};
}]);

@ -1,7 +1,8 @@
angular.module('openproject.uiComponents')
angular.module('openproject.helpers')
.constant('CUSTOM_FIELD_PREFIX', 'cf_')
.service('CustomFieldHelper', ['CUSTOM_FIELD_PREFIX', 'I18n', function(CUSTOM_FIELD_PREFIX, I18n) {
CustomFieldHelper = {
isCustomFieldKey: function(key) {
return key.substr(0, CUSTOM_FIELD_PREFIX.length) === CUSTOM_FIELD_PREFIX;
@ -23,9 +24,18 @@ angular.module('openproject.uiComponents')
case 'bool':
return CustomFieldHelper.booleanCustomFieldValue(value);
case 'user':
if (users[value])
if (users && users[value]) {
// try to look up users
return users[value].name;
} else {
// return user id
return value;
}
break;
case 'int':
return parseInt(value, 10);
case 'float':
return parseFloat(value);
default:
return value;
}

@ -0,0 +1,18 @@
// TODO move to UI components
angular.module('openproject.helpers')
.service('FunctionDecorators', ['$timeout', function($timeout) {
var currentRun;
return {
withDelay: function(delay, callback, params) {
$timeout.cancel(currentRun);
currentRun = $timeout(function() {
return callback.apply(this, params);
}, delay);
return currentRun;
}
};
}]);

@ -1,8 +1,10 @@
// TODO forward rails routes
angular.module('openproject.uiComponents')
angular.module('openproject.helpers')
.service('PathHelper', [function() {
PathHelper = {
apiPrefix: '/api/v2',
projectPath: function(projectIdentifier) {
return '/projects/' + projectIdentifier;
},
@ -15,14 +17,20 @@ angular.module('openproject.uiComponents')
projectWorkPackagesPath: function(projectIdentifier) {
return PathHelper.projectPath(projectIdentifier) + PathHelper.workPackagesPath();
},
usersPath: function() {
return '/users';
},
userPath: function(id) {
return '/users/' + id;
return PathHelper.usersPath() + id;
},
workPackagesColumnDataPath: function() {
return PathHelper.workPackagesPath() + '/column_data';
},
workPackagesSumsPath: function(projectIdentifier) {
return PathHelper.projectPath(projectIdentifier) + '/column_sums';
},
versionPath: function(versionId) {
return '/versions/' + versionId;
}
};

@ -1,6 +1,6 @@
angular.module('openproject.uiComponents')
angular.module('openproject.workPackages.helpers')
.factory('WorkPackagesHelper', [function() {
.factory('WorkPackagesHelper', ['dateFilter', 'CustomFieldHelper', function(dateFilter, CustomFieldHelper) {
var WorkPackagesHelper = {
getRowObjectContent: function(object, option) {
var content = object[option];
@ -33,30 +33,37 @@ angular.module('openproject.uiComponents')
return customValue.custom_field_id === customField.id;
}).first();
return WorkPackagesHelper.getCustomValue(customField, customValue);
},
getCustomValue: function(customField, customValue) {
if (!customValue) return '';
switch(customField.field_format) {
case 'int':
return parseInt(customValue.value);
case 'float':
return parseFloat(customValue.value);
default:
return customValue.value;
if(customValue) {
return CustomFieldHelper.formatCustomFieldValue(customValue.value, customField.field_format);
}
},
getColumnValue: function(rowObject, column) {
getFormattedColumnValue: function(rowObject, column) {
var value;
if (column.custom_field) {
return WorkPackagesHelper.getRowObjectCustomValue(rowObject, column.custom_field);
} else {
return WorkPackagesHelper.getRowObjectContent(rowObject, column.name);
value = WorkPackagesHelper.getRowObjectContent(rowObject, column.name);
return WorkPackagesHelper.formatValue(value, column.meta_data.data_type);
}
},
formatValue: function(value, dataType) {
switch(dataType) {
case 'datetime':
return dateFilter(WorkPackagesHelper.parseDateTime(value), 'medium');
case 'date':
return dateFilter(value, 'mediumDate');
default:
return value;
}
},
parseDateTime: function(value) {
return new Date(Date.parse(value.replace(/(A|P)M$/, '')));
},
projectRowsToColumn: function(rows, column) {
return rows.map(function(row){
return WorkPackagesHelper.getColumnValue(row.object, column);

@ -1,6 +1,6 @@
//-- copyright
// OpenProject is a project management system.
// Copyright (C) 2012-2013 the OpenProject Foundation (OPF)
// Copyright (C) 2012-2014 the OpenProject Foundation (OPF)
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License version 3.

@ -37,7 +37,9 @@
// │ OpenProject timelines module. │
// ╰───────────────────────────────────────────────────────────────╯
openprojectApp.factory('SvgHelper', [function() {
angular.module('openproject.helpers')
.factory('SvgHelper', [function() {
var SvgHelper = function(node) {
this.root = this.provideNode('svg').attr({

@ -1,6 +1,6 @@
// //-- copyright
// // OpenProject is a project management system.
// // Copyright (C) 2012-2013 the OpenProject Foundation (OPF)
// // Copyright (C) 2012-2014 the OpenProject Foundation (OPF)
// //
// // This program is free software; you can redistribute it and/or
// // modify it under the terms of the GNU General Public License version 3.

@ -1,6 +1,6 @@
//-- copyright
// OpenProject is a project management system.
// Copyright (C) 2012-2013 the OpenProject Foundation (OPF)
// Copyright (C) 2012-2014 the OpenProject Foundation (OPF)
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License version 3.

@ -1,6 +1,6 @@
//-- copyright
// OpenProject is a project management system.
// Copyright (C) 2012-2013 the OpenProject Foundation (OPF)
// Copyright (C) 2012-2014 the OpenProject Foundation (OPF)
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License version 3.

@ -1,21 +1,22 @@
// global
angular.module('openproject.services', ['openproject.uiComponents']);
angular.module('openproject.services', ['openproject.uiComponents', 'openproject.helpers']);
angular.module('openproject.helpers', ['openproject.services']);
angular.module('openproject.models', []);
// timelines
angular.module('openproject.timelines', ['openproject.timelines.controllers', 'openproject.timelines.directives', 'openproject.uiComponents']);
angular.module('openproject.timelines.models', []);
angular.module('openproject.timelines.models', ['openproject.helpers']);
angular.module('openproject.timelines.helpers', []);
angular.module('openproject.timelines.controllers', ['openproject.timelines.models']);
angular.module('openproject.timelines.services', ['openproject.timelines.models', 'openproject.timelines.helpers']);
angular.module('openproject.timelines.directives', ['openproject.timelines.models', 'openproject.timelines.services', 'openproject.uiComponents']);
angular.module('openproject.timelines.directives', ['openproject.timelines.models', 'openproject.timelines.services', 'openproject.uiComponents', 'openproject.helpers']);
// work packages
angular.module('openproject.workPackages', ['openproject.workPackages.controllers', 'openproject.workPackages.filters', 'openproject.workPackages.directives']);
angular.module('openproject.workPackages.helpers', ['openproject.uiComponents']);
angular.module('openproject.workPackages', ['openproject.workPackages.controllers', 'openproject.workPackages.filters', 'openproject.workPackages.directives', 'openproject.uiComponents']);
angular.module('openproject.workPackages.helpers', ['openproject.helpers']);
angular.module('openproject.workPackages.filters', ['openproject.workPackages.helpers']);
angular.module('openproject.workPackages.controllers', ['openproject.models', 'openproject.workPackages.helpers', 'openproject.services']);
angular.module('openproject.workPackages.directives', ['openproject.uiComponents', 'openproject.services']);
angular.module('openproject.workPackages.directives', ['openproject.helpers', 'openproject.workPackages.helpers', 'openproject.services']);
// main app
var openprojectApp = angular.module('openproject', ['ui.select2', 'ui.date', 'openproject.uiComponents', 'openproject.timelines', 'openproject.workPackages', 'ngAnimate']);

@ -1,6 +1,6 @@
//-- copyright
// OpenProject is a project management system.
// Copyright (C) 2012-2013 the OpenProject Foundation (OPF)
// Copyright (C) 2012-2014 the OpenProject Foundation (OPF)
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License version 3.

@ -0,0 +1,44 @@
angular.module('openproject.services')
.service('UserService', ['$http', 'PathHelper', 'FunctionDecorators', function($http, PathHelper, FunctionDecorators) {
var registeredUserIds = [], cachedUsers = {};
UserService = {
registerUserId: function(id) {
var user = cachedUsers[id];
if (user) return user;
registeredUserIds.push(id);
cachedUsers[id] = { name: '', firstname: '', lastname: '' }; // create an empty object and fill its values on load
FunctionDecorators.withDelay(10, UserService.loadRegisteredUsers); // HACK
// TODO hook into a given promise chain to post-load user data, or if ngView is used trigger load on $viewContentLoaded
return cachedUsers[id];
},
loadRegisteredUsers: function() {
if (registeredUserIds.length > 0) {
return $http.get(PathHelper.apiPrefix + PathHelper.usersPath(), {
params: { 'ids[]': registeredUserIds }
}).then(function(response){
UserService.storeUsers(response.data.users);
return cachedUsers;
});
}
},
storeUsers: function(users) {
// writes user data to object stubs providing a mechanism for wiring up user data to the scope
angular.forEach(users, function(user) {
var cachedUser = cachedUsers[user.id];
cachedUser.firstname = user.firstname;
cachedUser.lastname = user.lastname;
cachedUser.name = user.name;
});
}
};
return UserService;
}]);

@ -575,6 +575,7 @@ div.tooltip:hover span.tip, div.tooltip.hover span.tip {
#content table.progress td.done { background: #DEF0DE none repeat scroll 0%; }
#content table.progress td.open { background: #FFF none repeat scroll 0%; }
p.pourcent {font-size: 80%;}
p.progress-bar-legend {font-size: 80%; float: left;}
p.progress-info {clear: left; font-style: italic; font-size: 80%;}
/***** Tabs *****/

@ -51,23 +51,25 @@ class BoardsController < ApplicationController
end
def show
sort_init 'updated_on', 'desc'
sort_update 'created_on' => "#{Message.table_name}.created_on",
'replies' => "#{Message.table_name}.replies_count",
'updated_on' => "#{Message.table_name}.updated_on"
respond_to do |format|
format.html {
sort_init 'updated_on', 'desc'
sort_update 'created_on' => "#{Message.table_name}.created_on",
'replies' => "#{Message.table_name}.replies_count",
'updated_on' => "#{Message.table_name}.updated_on"
@topics = @board.topics.order(["#{Message.table_name}.sticky DESC", sort_clause].compact.join(', '))
@topics = @board.topics.order(["#{Message.table_name}.sticked_on ASC", sort_clause].compact.join(', '))
.includes(:author, { :last_reply => :author })
.page(params[:page])
.per_page(per_page_param)
@message = Message.new
render :action => 'show', :layout => !request.xhr?
}
format.atom {
@messages = @board.messages.order('created_on DESC')
@messages = @board.messages.order(["#{Message.table_name}.sticked_on ASC", sort_clause].compact.join(', '))
.includes(:author, :board)
.limit(Setting.feeds_limit.to_i)

@ -50,10 +50,10 @@ class MessagesController < ApplicationController
page = 1 + offset / REPLIES_PER_PAGE
end
@replies = @topic.children.includes(:author, :attachments, {:board => :project})
.order("#{Message.table_name}.created_on ASC")
.page(page)
.per_page(per_page_param)
@replies = @topic.children.includes(:author, :attachments, {:board => :project})
.order("#{Message.table_name}.created_on ASC")
.page(page)
.per_page(per_page_param)
@reply = Message.new(:subject => "RE: #{@message.subject}")
render :action => "show", :layout => !request.xhr?

@ -286,41 +286,53 @@ class WorkPackagesController < ApplicationController
column_names = params[:column_names]
project = Project.find_visible(current_user, params[:id])
work_packages = project.work_packages
sums = column_names.map do |column_name|
column_is_numeric?(column_name) ? fetch_column_data(column_name, work_packages).map{|c| c.nil? ? 0 : c}.sum : nil
fetch_column_data(column_name, work_packages).map{|c| c.nil? ? 0 : c}.compact.sum if column_should_be_summed_up?(column_name)
end
render json: sums
end
def fetch_columns_data(column_names, work_packages)
columns = column_names.map do |column_name|
column_names.map do |column_name|
fetch_column_data(column_name, work_packages)
end
end
def fetch_column_data(column_name, work_packages)
column = if column_name =~ /cf_(.*)/
work_packages.map do |work_package|
value = work_package.custom_values.find_by_custom_field_id($1) and value.nil? ? {} : value.attributes
end
else
work_packages.map do |work_package|
# Note: Doing as_json here because if we just take the value.attributes then we can't get any methods later.
# Name and subject are the default properties that the front end currently looks for to summarize an object.
value = work_package.send(column_name) and value.is_a?(ActiveRecord::Base) ? value.as_json( only: "id", methods: [:name, :subject] ) : value
end
if column_name =~ /cf_(.*)/
custom_field = CustomField.find($1)
work_packages.map do |work_package|
custom_value = work_package.custom_values.find_by_custom_field_id($1)
custom_field.cast_value custom_value.try(:value)
end
else
work_packages.map do |work_package|
# Note: Doing as_json here because if we just take the value.attributes then we can't get any methods later.
# Name and subject are the default properties that the front end currently looks for to summarize an object.
value = work_package.send(column_name)
value.is_a?(ActiveRecord::Base) ? value.as_json( only: "id", methods: [:name, :subject] ) : value
end
end
end
def column_should_be_summed_up?(column_name)
# see ::Query::Sums mix in
column_is_numeric?(column_name) && Setting.work_package_list_summable_columns.include?(column_name.to_s)
end
def column_is_numeric?(column_name)
# TODO RS: We want to leave out ids even though they are numeric
[:integer, :float].include? column_type(column_name)
[:int, :float].include? column_type(column_name)
end
def column_type(column_name)
column_name =~ /cf_(.*)/ ? CustomField.find($1).field_format.to_sym : (c = WorkPackage.columns_hash[column_name] and c.nil? ? :none : c.type)
if column_name =~ /cf_(.*)/
CustomField.find($1).field_format.to_sym
else
column = WorkPackage.columns_hash[column_name]
column.nil? ? :none : column.type
end
end
@ -535,7 +547,6 @@ class WorkPackagesController < ApplicationController
def push_query_and_results_via_gon(results, work_packages)
get_query_and_results_as_json(results, work_packages).each_pair do |name, value|
# binding.pry if name == :query
gon.send "#{name}=", value
end
# TODO later versions of gon support gon.push {Hash} - on the other hand they make it harder to deliver data to gon inside views
@ -603,7 +614,7 @@ class WorkPackagesController < ApplicationController
# Needs to be things like user link,project link, datetime
{
data_type: column_data_type(column),
link: !!(link_meta()[column.name]) ? link_meta()[column.name] : { display: false }
link: !!(link_meta[column.name]) ? link_meta()[column.name] : { display: false }
}
end
@ -617,7 +628,8 @@ class WorkPackagesController < ApplicationController
assigned_to: { display: true, model_type: "user" },
responsible: { display: true, model_type: "user" },
author: { display: true, model_type: "user" },
project: { display: true, model_type: "project" }
project: { display: true, model_type: "project" },
fixed_version: { display: true, model_type: "version" }
}
end
@ -654,7 +666,8 @@ class WorkPackagesController < ApplicationController
responsible: { only: :id, methods: :name },
status: { only: :name },
type: { only: :name },
parent: { only: :subject }
parent: { only: :subject },
fixed_version: { only: [:name, :id] }
}.slice(*selected_columns.map(&:name))
selected_associations.merge!(custom_values: { only: [:custom_field_id, :value] }) if selected_columns.any? {|c| c.is_a? QueryCustomFieldColumn}

@ -31,8 +31,8 @@ class Board < ActiveRecord::Base
include ActiveModel::ForbiddenAttributesProtection
belongs_to :project
has_many :topics, :class_name => 'Message', :conditions => "#{Message.table_name}.parent_id IS NULL", :order => "#{Message.table_name}.created_on DESC"
has_many :messages, :dependent => :destroy, :order => "#{Message.table_name}.created_on DESC"
has_many :topics, :class_name => 'Message', :conditions => "#{Message.table_name}.parent_id IS NULL" , :order => "#{Message.table_name}.sticky DESC"
has_many :messages, :dependent => :destroy , :order => "#{Message.table_name}.sticky DESC"
belongs_to :last_message, :class_name => 'Message', :foreign_key => :last_message_id
acts_as_list :scope => :project_id
acts_as_watchable

@ -85,11 +85,21 @@ class Message < ActiveRecord::Base
validate :validate_unlocked_root, :on => :create
before_save :set_sticked_on_date
# Can not reply to a locked topic
def validate_unlocked_root
errors.add :base, 'Topic is locked' if root.locked? && self != root
end
def set_sticked_on_date
if sticky?
self.sticked_on = sticked_on.nil? ? Time.now : sticked_on
else
self.sticked_on = nil
end
end
def update_last_reply_in_parent
if parent
parent.reload.update_attribute(:last_reply_id, self.id)

@ -56,7 +56,7 @@ See doc/COPYRIGHT.rdoc for more details.
<div class="message">
<p><span class="author"><%= authoring @topic.created_on, @topic.author %></span></p>
<div class="wiki">
<%= textilizable(@topic.content, :attachments => @topic.attachments) %>
<%= textilizable(@topic.content, :object => @topic, :attachments => @topic.attachments) %>
</div>
<%= link_to_attachments @topic, :author => false %>
</div>

@ -38,7 +38,7 @@ See doc/COPYRIGHT.rdoc for more details.
<%= "(#{l(:label_x_comments, :count => news.comments_count)})" if news.comments_count > 0 %></h3>
<p class="author"><%= authoring news.created_on, news.author %></p>
<div class="wiki">
<%= textilizable(news.summary.present? ? news.summary : truncate(news.description)) %>
<%= textilizable(news.summary.present? ? news.summary : truncate(news.description), :object => news) %>
</div>
<% end %>
<% end %>

@ -64,7 +64,7 @@ See doc/COPYRIGHT.rdoc for more details.
<p><% unless @news.summary.blank? %><em><%=h @news.summary %></em><br /><% end %>
<span class="author"><%= authoring @news.created_on, @news.author %></span></p>
<div class="wiki">
<%= textilizable(@news.description) %>
<%= textilizable(@news.description, :object => @news) %>
</div>
<br />
@ -82,7 +82,7 @@ See doc/COPYRIGHT.rdoc for more details.
:alt => l(:button_delete) %>
</div>
<h4 class="comment"><%= avatar(comment.author, :size => "24") %><%= authoring comment.created_on, comment.author %></h4>
<%= textilizable(comment.comments) %>
<%= textilizable(comment.comments, :object => comment) %>
<% end %>
</div>

@ -0,0 +1,38 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2014 the OpenProject Foundation (OPF)
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See doc/COPYRIGHT.rdoc for more details.
#++
class FixCustomizableJournalValueColumn < ActiveRecord::Migration
def up
change_column :customizable_journals, :value, :text
end
def down
change_column :customizable_journals, :value, :string
end
end

@ -0,0 +1,34 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2014 the OpenProject Foundation (OPF)
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See doc/COPYRIGHT.rdoc for more details.
#++
class AddStickedOnFieldToMessages < ActiveRecord::Migration
def change
add_column :messages, :sticked_on, :datetime, default: nil, null: true
end
end

@ -29,11 +29,13 @@ See doc/COPYRIGHT.rdoc for more details.
# Changelog
* `#284` Fix: Sticky does not apply to forum
* `#2393` Fix: No warning when leaving site without saving
* `#2401` Fix: New target version cannot be created from work package view
* `#3267` Fix: Link in Breadcrumbs links to global work packages
* `#3395` Fix: After error message values are gone during creation of message
* `#3531` Fix: Type 'None' cannot be configured via admin settings
* `#4040` Fix: Referencing work packages with ### in news, forums and meetings does not work
* `#4087` Ignore type list flash when activating flash messages
* `#4097` Fix accesskeys
* `#4118` Fix: Add missing labels

@ -27,23 +27,24 @@
#++
Feature: Issue textile quickinfo links
Background:
Given there is 1 project with the following:
| name | parent |
| identifier | parent |
| name | parent |
| identifier | parent |
And I am working in project "parent"
And there is a board "development discussion" for project "parent"
And there is a role "member"
And the role "member" may have the following rights:
| manage_boards |
| add_messages |
| edit_messages |
| edit_own_messages |
| delete_messages |
| delete_messages |
| delete_own_messages |
| manage_boards |
| add_messages |
| edit_messages |
| edit_own_messages |
| delete_messages |
| delete_messages |
| delete_own_messages |
And there is 1 user with the following:
| login | bob|
| login | bob |
And the user "bob" is a "member" in the project "parent"
And I am already logged in as "bob"
@ -65,7 +66,7 @@ Feature: Issue textile quickinfo links
Scenario: Message's reply count is two
Given the board "development discussion" has the following messages:
| message #1 |
And "message #1" has the following replies:
And "message #1" has the following replies:
| reply #1 |
| reply #2 |
When I go to the message page of message "message #1"
@ -85,4 +86,18 @@ Feature: Issue textile quickinfo links
And I fill in "Here you find the most frequently asked questions" for "message_content"
When I click on the first button matching "Create"
Then there should be an error message
Then the "message_content" field should contain "Here you find the most frequently asked questions"
Then the "message_content" field should contain "Here you find the most frequently asked questions"
Scenario: Sticky message on top of messages list
Given the board "development discussion" has the following messages:
| message #1 |
| message #2 |
| message #3 |
When I go to the boards page of the project called "parent"
And I follow "New message"
And I fill in "How to?" for "message_subject"
And I fill in "How to st-up project on local mashine." for "message_content"
And I check "Sticky"
When I click on the first button matching "Create"
And I go to the boards page of the project called "parent"
Then "How to?" should be the first row in table

@ -1,7 +1,7 @@
#-- copyright
# OpenProject is a project management system.
#
# Copyright (C) 2012-2013 the OpenProject Team
# Copyright (C) 2012-2014 the OpenProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.

@ -1,6 +1,6 @@
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2013 the OpenProject Foundation (OPF)
# Copyright (C) 2012-2014 the OpenProject Foundation (OPF)
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.

@ -75,3 +75,6 @@ Then(/^I should see the following fields:$/) do |table|
end
end
Then(/^"([^"]*)" should be the first row in table$/) do |name|
should have_selector("table.list tbody tr td", :text => Regexp.new("#{name}"))
end

@ -1,6 +1,6 @@
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2013 the OpenProject Foundation (OPF)
# Copyright (C) 2012-2014 the OpenProject Foundation (OPF)
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.

@ -1,6 +1,6 @@
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2013 the OpenProject Foundation (OPF)
# Copyright (C) 2012-2014 the OpenProject Foundation (OPF)
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.

@ -1,6 +1,6 @@
#-- copyright
# OpenProject is a project management system.
# Copyright (C) 2012-2013 the OpenProject Foundation (OPF)
# Copyright (C) 2012-2014 the OpenProject Foundation (OPF)
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.

@ -0,0 +1,11 @@
<div>
<table class="progress" ng-style="{width: width}">
<tbody>
<tr>
<td class="closed" ng-if="progress > 0" ng-style="{width: progress + '%'}"></td>
<td class="todo" ng-style="{width: (scaleLength - progress) + '%'}"></td>
</tr>
</tbody>
</table>
<p class="progress-bar-legend">{{legend}} Total progress</p>
</div>

@ -1,6 +0,0 @@
<span ng-switch="displayType">
<span ng-switch-when="text">
{{ displayText }}
</span>
<a ng-switch-when="link" href="{{ url }}">{{ displayText }}</a>
</span>

@ -0,0 +1,12 @@
<span ng-switch="displayType">
<progress-bar ng-switch-when="progress_bar"
progress="displayText"
width="80px">
</progress-bar>
<a ng-switch-when="link" href="{{ url }}">{{ displayText }}</a>
<span ng-switch-default>
{{ displayText }}
</span>
</span>

@ -13,18 +13,17 @@
</th>
<th sort-header
header-name="id"
header-title="#"
header-name="'id'"
header-title="'#'"
sortable="true"
query="query"
update-results="updateResults()"/>
<th sort-header ng-repeat="column in columns"
header-name="{{column.name}}"
header-title="{{column.title}}"
sortable="{{column.sortable}}"
header-name="column.name"
header-title="column.title"
sortable="column.sortable"
query="query"
column="column"
update-results="updateResults()"/>
</tr>
</thead>
@ -108,8 +107,11 @@
<a ng-href="/work_packages/{{row.object.id}}">{{row.object.id}}</a>
</td>
<td ng-repeat="column in columns" class="{{column.name}}" lang-attribute>
<span work-package-column work-package="row.object" column="column"/>
<td ng-repeat="column in columns" class="{{column.name}}">
<span work-package-column
work-package="row.object"
column="column"
display-type="{{column.meta_data.data_type}}"/>
</td>
</tr>

@ -175,4 +175,63 @@ describe BoardsController do
end
end
describe :sticky do
let!(:message1) { FactoryGirl.create(:message, board: board) }
let!(:message2) { FactoryGirl.create(:message, board: board) }
let!(:sticked_message1) { FactoryGirl.create(:message, board_id: board.id, subject: "How to",
content: "How to install this cool app", sticky: "1", sticked_on: Time.now - 2.minute) }
let!(:sticked_message2) { FactoryGirl.create(:message, board_id: board.id, subject: "FAQ",
content: "Frequestly asked question", sticky: "1", sticked_on: Time.now - 1.minute) }
describe "all sticky messages" do
before do
@controller.should_receive(:authorize)
get :show, project_id: project.id, id: board.id
end
it { expect(response).to render_template 'show' }
it "should be displayed on top" do
expect(assigns[:topics][0].id).to eq(sticked_message1.id)
end
end
describe "edit a sticky message" do
before(:each) do
sticked_message1.sticky = 0
sticked_message1.save!
end
describe "when sticky is unset from message" do
before do
@controller.should_receive(:authorize)
get :show, project_id: project.id, id: board.id
end
it "it should not be displayed as sticky message" do
expect(sticked_message1.sticked_on).to be_nil
expect(assigns[:topics][0].id).to_not eq(sticked_message1.id)
end
end
describe "when sticky is set back to message" do
before do
sticked_message1.sticky = 1
sticked_message1.save!
@controller.should_receive(:authorize)
get :show, project_id: project.id, id: board.id
end
it "it should not be displayed on first position" do
expect(assigns[:topics][0].id).to eq(sticked_message2.id)
end
end
end
end
end

@ -229,5 +229,27 @@ describe WorkPackage do
it { should eq('true') }
end
end
describe "custom field type 'text'" do
let(:value) { "text" * 1024 }
let(:custom_field) { FactoryGirl.create(:work_package_custom_field,
name: 'Test Text',
field_format: 'text',
is_required: true) }
include_context "project with required custom field"
it_behaves_like "work package with required custom field"
describe 'value' do
before do
change_custom_field_value(work_package, value)
end
subject { work_package.journals.first.customizable_journals.first.value }
it { expect(subject).to eq(value) }
end
end
end
end

Loading…
Cancel
Save