summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
authorDouglas Barbosa Alexandre <dbalexandre@gmail.com>2016-02-22 02:36:41 +0000
committerDouglas Barbosa Alexandre <dbalexandre@gmail.com>2016-02-22 02:36:41 +0000
commit04d1b412587028260ac219a32bdf3b03ac022308 (patch)
tree7486a02a074be0444320abc5b67837f9d2b39cb8 /app
parent28f9b54372cd2d7b7ac930c2a4968ca435382851 (diff)
parent2ef1ea501f48e29399d94ec5843984e4f7014b99 (diff)
downloadgitlab-ce-04d1b412587028260ac219a32bdf3b03ac022308.tar.gz
Merge branch 'tasks' into 'master'
Add Todos Closes https://gitlab.com/gitlab-org/gitlab-ce/issues/2425 Tasks: - Prepare database - [X] Create a new table (`todos`) - Tasks Queue view - [X] Add a number icon showing the number of todos on the top right next to the new and logout button that will redirect the user to the todos page - [X] Add a chronological list of todos, with the 'Todos' tab active by default - [X] Add a 'Done' button to each todo - [x] Add filters (project, author, type, and action) - Todos generation - [X] When user issue/mr is assgined to someone - [x] When user is mentioned on (issues/mr's/comments) - Mark todo as `done` - [X] When clicks on the 'Done' button - [X] When edit issue/mr - [X] When left/edit a comment - [X] When reassign issue/mr - [X] When add/remove labels to issue/mr - [X] When issue/mr is closed - [X] When mr is merged - [X] When added an emoji - [X] When changed the issue/mr milestone * Screenshot: ![Screenshot_2016-02-20_12.45.57](/uploads/4b2554b1bde25aed3347e1ae41e8e0c0/Screenshot_2016-02-20_12.45.57.png) See merge request !2817
Diffstat (limited to 'app')
-rw-r--r--app/assets/stylesheets/pages/todos.scss124
-rw-r--r--app/controllers/dashboard/todos_controller.rb35
-rw-r--r--app/controllers/projects/merge_requests_controller.rb2
-rw-r--r--app/finders/todos_finder.rb129
-rw-r--r--app/helpers/todos_helper.rb87
-rw-r--r--app/models/note.rb2
-rw-r--r--app/models/todo.rb53
-rw-r--r--app/models/user.rb2
-rw-r--r--app/services/base_service.rb4
-rw-r--r--app/services/issuable_base_service.rb15
-rw-r--r--app/services/issues/close_service.rb2
-rw-r--r--app/services/issues/create_service.rb1
-rw-r--r--app/services/issues/update_service.rb12
-rw-r--r--app/services/merge_requests/close_service.rb1
-rw-r--r--app/services/merge_requests/create_service.rb3
-rw-r--r--app/services/merge_requests/update_service.rb12
-rw-r--r--app/services/notes/create_service.rb1
-rw-r--r--app/services/notes/post_process_service.rb2
-rw-r--r--app/services/notes/update_service.rb4
-rw-r--r--app/services/todo_service.rb170
-rw-r--r--app/views/dashboard/todos/_todo.html.haml21
-rw-r--r--app/views/dashboard/todos/index.html.haml62
-rw-r--r--app/views/layouts/header/_default.html.haml6
-rw-r--r--app/views/layouts/nav/_dashboard.html.haml18
24 files changed, 754 insertions, 14 deletions
diff --git a/app/assets/stylesheets/pages/todos.scss b/app/assets/stylesheets/pages/todos.scss
new file mode 100644
index 00000000000..2f57f21963d
--- /dev/null
+++ b/app/assets/stylesheets/pages/todos.scss
@@ -0,0 +1,124 @@
+/**
+ * Dashboard Todos
+ *
+ */
+
+.navbar-nav {
+ li {
+ .badge.todos-pending-count {
+ background-color: #7f8fa4;
+ margin-top: -5px;
+ }
+ }
+}
+
+.todos {
+ .panel {
+ border-top: none;
+ margin-bottom: 0;
+ }
+}
+
+.todo-item {
+ font-size: $gl-font-size;
+ padding: $gl-padding-top 0 $gl-padding-top ($gl-avatar-size + $gl-padding-top);
+ border-bottom: 1px solid $table-border-color;
+ color: #7f8fa4;
+
+ &.todo-inline {
+ .avatar {
+ position: relative;
+ top: -2px;
+ }
+
+ .todo-title {
+ line-height: 40px;
+ }
+ }
+
+ a {
+ color: #4c4e54;
+ }
+
+ .avatar {
+ margin-left: -($gl-avatar-size + $gl-padding-top);
+ }
+
+ .todo-title {
+ @include str-truncated(calc(100% - 174px));
+ font-weight: 600;
+
+ .author_name {
+ color: #333;
+ }
+ }
+
+ .todo-body {
+ margin-right: 174px;
+
+ .todo-note {
+ word-wrap: break-word;
+
+ .md {
+ color: #7f8fa4;
+ font-size: $gl-font-size;
+
+ p {
+ color: #5c5d5e;
+ }
+ }
+
+ pre {
+ border: none;
+ background: #f9f9f9;
+ border-radius: 0;
+ color: #777;
+ margin: 0 20px;
+ overflow: hidden;
+ }
+
+ .note-image-attach {
+ margin-top: 4px;
+ margin-left: 0px;
+ max-width: 200px;
+ float: none;
+ }
+
+ p:last-child {
+ margin-bottom: 0;
+ }
+ }
+
+ .todo-note-icon {
+ color: #777;
+ float: left;
+ font-size: $gl-font-size;
+ line-height: 16px;
+ margin-right: 5px;
+ }
+ }
+
+ &:last-child { border:none }
+}
+
+@media (max-width: $screen-xs-max) {
+ .todo-item {
+ padding-left: $gl-padding;
+
+ .todo-title {
+ white-space: normal;
+ overflow: visible;
+ max-width: 100%;
+ }
+
+ .avatar {
+ display: none;
+ }
+
+ .todo-body {
+ margin: 0;
+ border-left: 2px solid #DDD;
+ padding-left: 10px;
+ }
+ }
+}
diff --git a/app/controllers/dashboard/todos_controller.rb b/app/controllers/dashboard/todos_controller.rb
new file mode 100644
index 00000000000..9ee9039f004
--- /dev/null
+++ b/app/controllers/dashboard/todos_controller.rb
@@ -0,0 +1,35 @@
+class Dashboard::TodosController < Dashboard::ApplicationController
+ before_action :find_todos, only: [:index, :destroy_all]
+
+ def index
+ @todos = @todos.page(params[:page]).per(PER_PAGE)
+ end
+
+ def destroy
+ todo.done!
+
+ respond_to do |format|
+ format.html { redirect_to dashboard_todos_path, notice: 'Todo was successfully marked as done.' }
+ format.js { render nothing: true }
+ end
+ end
+
+ def destroy_all
+ @todos.each(&:done)
+
+ respond_to do |format|
+ format.html { redirect_to dashboard_todos_path, notice: 'All todos were marked as done.' }
+ format.js { render nothing: true }
+ end
+ end
+
+ private
+
+ def todo
+ @todo ||= current_user.todos.find(params[:id])
+ end
+
+ def find_todos
+ @todos = TodosFinder.new(current_user, params).execute
+ end
+end
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index 86b8e7bdf2e..5fe21694605 100644
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -181,6 +181,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController
return
end
+ TodoService.new.merge_merge_request(merge_request, current_user)
+
@merge_request.update(merge_error: nil)
if params[:merge_when_build_succeeds].present? && @merge_request.ci_commit && @merge_request.ci_commit.active?
diff --git a/app/finders/todos_finder.rb b/app/finders/todos_finder.rb
new file mode 100644
index 00000000000..3ba27c40504
--- /dev/null
+++ b/app/finders/todos_finder.rb
@@ -0,0 +1,129 @@
+# TodosFinder
+#
+# Used to filter Todos by set of params
+#
+# Arguments:
+# current_user - which user use
+# params:
+# action_id: integer
+# author_id: integer
+# project_id; integer
+# state: 'pending' or 'done'
+# type: 'Issue' or 'MergeRequest'
+#
+
+class TodosFinder
+ NONE = '0'
+
+ attr_accessor :current_user, :params
+
+ def initialize(current_user, params)
+ @current_user = current_user
+ @params = params
+ end
+
+ def execute
+ items = current_user.todos
+ items = by_action_id(items)
+ items = by_author(items)
+ items = by_project(items)
+ items = by_state(items)
+ items = by_type(items)
+
+ items
+ end
+
+ private
+
+ def action_id?
+ action_id.present? && [Todo::ASSIGNED, Todo::MENTIONED].include?(action_id.to_i)
+ end
+
+ def action_id
+ params[:action_id]
+ end
+
+ def author?
+ params[:author_id].present?
+ end
+
+ def author
+ return @author if defined?(@author)
+
+ @author =
+ if author? && params[:author_id] != NONE
+ User.find(params[:author_id])
+ else
+ nil
+ end
+ end
+
+ def project?
+ params[:project_id].present?
+ end
+
+ def project
+ return @project if defined?(@project)
+
+ if project?
+ @project = Project.find(params[:project_id])
+
+ unless Ability.abilities.allowed?(current_user, :read_project, @project)
+ @project = nil
+ end
+ else
+ @project = nil
+ end
+
+ @project
+ end
+
+ def type?
+ type.present? && ['Issue', 'MergeRequest'].include?(type)
+ end
+
+ def type
+ params[:type]
+ end
+
+ def by_action_id(items)
+ if action_id?
+ items = items.where(action: action_id)
+ end
+
+ items
+ end
+
+ def by_author(items)
+ if author?
+ items = items.where(author_id: author.try(:id))
+ end
+
+ items
+ end
+
+ def by_project(items)
+ if project?
+ items = items.where(project: project)
+ end
+
+ items
+ end
+
+ def by_state(items)
+ case params[:state]
+ when 'done'
+ items.done
+ else
+ items.pending
+ end
+ end
+
+ def by_type(items)
+ if type?
+ items = items.where(target_type: type)
+ end
+
+ items
+ end
+end
diff --git a/app/helpers/todos_helper.rb b/app/helpers/todos_helper.rb
new file mode 100644
index 00000000000..4b745a5b969
--- /dev/null
+++ b/app/helpers/todos_helper.rb
@@ -0,0 +1,87 @@
+module TodosHelper
+ def todos_pending_count
+ current_user.todos.pending.count
+ end
+
+ def todos_done_count
+ current_user.todos.done.count
+ end
+
+ def todo_action_name(todo)
+ case todo.action
+ when Todo::ASSIGNED then 'assigned you'
+ when Todo::MENTIONED then 'mentioned you on'
+ end
+ end
+
+ def todo_target_link(todo)
+ target = todo.target_type.titleize.downcase
+ link_to "#{target} #{todo.target.to_reference}", todo_target_path(todo)
+ end
+
+ def todo_target_path(todo)
+ anchor = dom_id(todo.note) if todo.note.present?
+
+ polymorphic_path([todo.project.namespace.becomes(Namespace),
+ todo.project, todo.target], anchor: anchor)
+ end
+
+ def todos_filter_params
+ {
+ state: params[:state],
+ project_id: params[:project_id],
+ author_id: params[:author_id],
+ type: params[:type],
+ action_id: params[:action_id],
+ }
+ end
+
+ def todos_filter_path(options = {})
+ without = options.delete(:without)
+
+ options = todos_filter_params.merge(options)
+
+ if without.present?
+ without.each do |key|
+ options.delete(key)
+ end
+ end
+
+ path = request.path
+ path << "?#{options.to_param}"
+ path
+ end
+
+ def todo_actions_options
+ actions = [
+ OpenStruct.new(id: '', title: 'Any Action'),
+ OpenStruct.new(id: Todo::ASSIGNED, title: 'Assigned'),
+ OpenStruct.new(id: Todo::MENTIONED, title: 'Mentioned')
+ ]
+
+ options_from_collection_for_select(actions, 'id', 'title', params[:action_id])
+ end
+
+ def todo_projects_options
+ projects = current_user.authorized_projects.sorted_by_activity.non_archived
+ projects = projects.includes(:namespace)
+
+ projects = projects.map do |project|
+ OpenStruct.new(id: project.id, title: project.name_with_namespace)
+ end
+
+ projects.unshift(OpenStruct.new(id: '', title: 'Any Project'))
+
+ options_from_collection_for_select(projects, 'id', 'title', params[:project_id])
+ end
+
+ def todo_types_options
+ types = [
+ OpenStruct.new(title: 'Any Type', name: ''),
+ OpenStruct.new(title: 'Issue', name: 'Issue'),
+ OpenStruct.new(title: 'Merge Request', name: 'MergeRequest')
+ ]
+
+ options_from_collection_for_select(types, 'name', 'title', params[:type])
+ end
+end
diff --git a/app/models/note.rb b/app/models/note.rb
index b3809ad81e0..d287e0f3c6d 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -37,6 +37,8 @@ class Note < ActiveRecord::Base
belongs_to :author, class_name: "User"
belongs_to :updated_by, class_name: "User"
+ has_many :todos, dependent: :destroy
+
delegate :name, to: :project, prefix: true
delegate :name, :email, to: :author, prefix: true
diff --git a/app/models/todo.rb b/app/models/todo.rb
new file mode 100644
index 00000000000..34d71c1b0d3
--- /dev/null
+++ b/app/models/todo.rb
@@ -0,0 +1,53 @@
+# == Schema Information
+#
+# Table name: todos
+#
+# id :integer not null, primary key
+# user_id :integer not null
+# project_id :integer not null
+# target_id :integer not null
+# target_type :string not null
+# author_id :integer
+# note_id :integer
+# action :integer not null
+# state :string not null
+# created_at :datetime
+# updated_at :datetime
+#
+
+class Todo < ActiveRecord::Base
+ ASSIGNED = 1
+ MENTIONED = 2
+
+ belongs_to :author, class_name: "User"
+ belongs_to :note
+ belongs_to :project
+ belongs_to :target, polymorphic: true, touch: true
+ belongs_to :user
+
+ delegate :name, :email, to: :author, prefix: true, allow_nil: true
+
+ validates :action, :project, :target, :user, presence: true
+
+ default_scope { reorder(id: :desc) }
+
+ scope :pending, -> { with_state(:pending) }
+ scope :done, -> { with_state(:done) }
+
+ state_machine :state, initial: :pending do
+ event :done do
+ transition pending: :done
+ end
+
+ state :pending
+ state :done
+ end
+
+ def body
+ if note.present?
+ note.note
+ else
+ target.title
+ end
+ end
+end
diff --git a/app/models/user.rb b/app/models/user.rb
index 9fe94b13e52..02ff2456f2b 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -140,7 +140,7 @@ class User < ActiveRecord::Base
has_one :abuse_report, dependent: :destroy
has_many :spam_logs, dependent: :destroy
has_many :builds, dependent: :nullify, class_name: 'Ci::Build'
-
+ has_many :todos, dependent: :destroy
#
# Validations
diff --git a/app/services/base_service.rb b/app/services/base_service.rb
index b48ca67d4d2..8563633816c 100644
--- a/app/services/base_service.rb
+++ b/app/services/base_service.rb
@@ -23,6 +23,10 @@ class BaseService
EventCreateService.new
end
+ def todo_service
+ TodoService.new
+ end
+
def log_info(message)
Gitlab::AppLogger.info message
end
diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb
index 2556f06e2d3..ca87dca4a70 100644
--- a/app/services/issuable_base_service.rb
+++ b/app/services/issuable_base_service.rb
@@ -54,7 +54,7 @@ class IssuableBaseService < BaseService
if params.present? && issuable.update_attributes(params.merge(updated_by: current_user))
issuable.reset_events_cache
handle_common_system_notes(issuable, old_labels: old_labels)
- handle_changes(issuable)
+ handle_changes(issuable, old_labels: old_labels)
issuable.create_new_cross_references!(current_user)
execute_hooks(issuable, 'update')
end
@@ -71,6 +71,19 @@ class IssuableBaseService < BaseService
end
end
+ def has_changes?(issuable, options = {})
+ valid_attrs = [:title, :description, :assignee_id, :milestone_id, :target_branch]
+
+ attrs_changed = valid_attrs.any? do |attr|
+ issuable.previous_changes.include?(attr.to_s)
+ end
+
+ old_labels = options[:old_labels]
+ labels_changed = old_labels && issuable.labels != old_labels
+
+ attrs_changed || labels_changed
+ end
+
def handle_common_system_notes(issuable, options = {})
if issuable.previous_changes.include?('title')
create_title_change_note(issuable, issuable.previous_changes['title'].first)
diff --git a/app/services/issues/close_service.rb b/app/services/issues/close_service.rb
index a1a20e47681..78254b49af3 100644
--- a/app/services/issues/close_service.rb
+++ b/app/services/issues/close_service.rb
@@ -3,6 +3,7 @@ module Issues
def execute(issue, commit = nil)
if project.jira_tracker? && project.jira_service.active
project.jira_service.execute(commit, issue)
+ todo_service.close_issue(issue, current_user)
return issue
end
@@ -10,6 +11,7 @@ module Issues
event_service.close_issue(issue, current_user)
create_note(issue, commit)
notification_service.close_issue(issue, current_user)
+ todo_service.close_issue(issue, current_user)
execute_hooks(issue, 'close')
end
diff --git a/app/services/issues/create_service.rb b/app/services/issues/create_service.rb
index bcb380d3215..10787e8873c 100644
--- a/app/services/issues/create_service.rb
+++ b/app/services/issues/create_service.rb
@@ -9,6 +9,7 @@ module Issues
if issue.save
issue.update_attributes(label_ids: label_params)
notification_service.new_issue(issue, current_user)
+ todo_service.new_issue(issue, current_user)
event_service.open_issue(issue, current_user)
issue.create_cross_references!(current_user)
execute_hooks(issue, 'open')
diff --git a/app/services/issues/update_service.rb b/app/services/issues/update_service.rb
index a55a04dd5e0..51ef9dfe610 100644
--- a/app/services/issues/update_service.rb
+++ b/app/services/issues/update_service.rb
@@ -4,7 +4,16 @@ module Issues
update(issue)
end
- def handle_changes(issue)
+ def handle_changes(issue, options = {})
+ if has_changes?(issue, options)
+ todo_service.mark_pending_todos_as_done(issue, current_user)
+ end
+
+ if issue.previous_changes.include?('title') ||
+ issue.previous_changes.include?('description')
+ todo_service.update_issue(issue, current_user)
+ end
+
if issue.previous_changes.include?('milestone_id')
create_milestone_note(issue)
end
@@ -12,6 +21,7 @@ module Issues
if issue.previous_changes.include?('assignee_id')
create_assignee_note(issue)
notification_service.reassigned_issue(issue, current_user)
+ todo_service.reassigned_issue(issue, current_user)
end
end
diff --git a/app/services/merge_requests/close_service.rb b/app/services/merge_requests/close_service.rb
index 47454f9f0c2..27ee81fe3e7 100644
--- a/app/services/merge_requests/close_service.rb
+++ b/app/services/merge_requests/close_service.rb
@@ -9,6 +9,7 @@ module MergeRequests
event_service.close_mr(merge_request, current_user)
create_note(merge_request)
notification_service.close_mr(merge_request, current_user)
+ todo_service.close_merge_request(merge_request, current_user)
execute_hooks(merge_request, 'close')
end
diff --git a/app/services/merge_requests/create_service.rb b/app/services/merge_requests/create_service.rb
index 009d5a6867e..33609d01f20 100644
--- a/app/services/merge_requests/create_service.rb
+++ b/app/services/merge_requests/create_service.rb
@@ -2,7 +2,7 @@ module MergeRequests
class CreateService < MergeRequests::BaseService
def execute
# @project is used to determine whether the user can set the merge request's
- # assignee, milestone and labels. Whether they can depends on their
+ # assignee, milestone and labels. Whether they can depends on their
# permissions on the target project.
source_project = @project
@project = Project.find(params[:target_project_id]) if params[:target_project_id]
@@ -18,6 +18,7 @@ module MergeRequests
merge_request.update_attributes(label_ids: label_params)
event_service.open_mr(merge_request, current_user)
notification_service.new_merge_request(merge_request, current_user)
+ todo_service.new_merge_request(merge_request, current_user)
merge_request.create_cross_references!(current_user)
execute_hooks(merge_request)
end
diff --git a/app/services/merge_requests/update_service.rb b/app/services/merge_requests/update_service.rb
index 5ff2cc03dda..6319ad805b6 100644
--- a/app/services/merge_requests/update_service.rb
+++ b/app/services/merge_requests/update_service.rb
@@ -14,7 +14,16 @@ module MergeRequests
update(merge_request)
end
- def handle_changes(merge_request)
+ def handle_changes(merge_request, options = {})
+ if has_changes?(merge_request, options)
+ todo_service.mark_pending_todos_as_done(merge_request, current_user)
+ end
+
+ if merge_request.previous_changes.include?('title') ||
+ merge_request.previous_changes.include?('description')
+ todo_service.update_merge_request(merge_request, current_user)
+ end
+
if merge_request.previous_changes.include?('target_branch')
create_branch_change_note(merge_request, 'target',
merge_request.previous_changes['target_branch'].first,
@@ -28,6 +37,7 @@ module MergeRequests
if merge_request.previous_changes.include?('assignee_id')
create_assignee_note(merge_request)
notification_service.reassigned_merge_request(merge_request, current_user)
+ todo_service.reassigned_merge_request(merge_request, current_user)
end
if merge_request.previous_changes.include?('target_branch') ||
diff --git a/app/services/notes/create_service.rb b/app/services/notes/create_service.rb
index 8d9661167b5..b970439b921 100644
--- a/app/services/notes/create_service.rb
+++ b/app/services/notes/create_service.rb
@@ -8,6 +8,7 @@ module Notes
if note.save
# Finish the harder work in the background
NewNoteWorker.perform_in(2.seconds, note.id, params)
+ TodoService.new.new_note(note, current_user)
end
note
diff --git a/app/services/notes/post_process_service.rb b/app/services/notes/post_process_service.rb
index f37d3c50cdd..e818f58d13c 100644
--- a/app/services/notes/post_process_service.rb
+++ b/app/services/notes/post_process_service.rb
@@ -1,6 +1,5 @@
module Notes
class PostProcessService
-
attr_accessor :note
def initialize(note)
@@ -25,6 +24,5 @@ module Notes
@note.project.execute_hooks(note_data, :note_hooks)
@note.project.execute_services(note_data, :note_hooks)
end
-
end
end
diff --git a/app/services/notes/update_service.rb b/app/services/notes/update_service.rb
index 72e2f78008d..1361b1e0300 100644
--- a/app/services/notes/update_service.rb
+++ b/app/services/notes/update_service.rb
@@ -7,6 +7,10 @@ module Notes
note.create_new_cross_references!(current_user)
note.reset_events_cache
+ if note.previous_changes.include?('note')
+ TodoService.new.update_note(note, current_user)
+ end
+
note
end
end
diff --git a/app/services/todo_service.rb b/app/services/todo_service.rb
new file mode 100644
index 00000000000..dc270602ebc
--- /dev/null
+++ b/app/services/todo_service.rb
@@ -0,0 +1,170 @@
+# TodoService class
+#
+# Used for creating todos after certain user actions
+#
+# Ex.
+# TodoService.new.new_issue(issue, current_user)
+#
+class TodoService
+ # When create an issue we should:
+ #
+ # * create a todo for assignee if issue is assigned
+ # * create a todo for each mentioned user on issue
+ #
+ def new_issue(issue, current_user)
+ new_issuable(issue, current_user)
+ end
+
+ # When update an issue we should:
+ #
+ # * mark all pending todos related to the issue for the current user as done
+ #
+ def update_issue(issue, current_user)
+ create_mention_todos(issue.project, issue, current_user)
+ end
+
+ # When close an issue we should:
+ #
+ # * mark all pending todos related to the target for the current user as done
+ #
+ def close_issue(issue, current_user)
+ mark_pending_todos_as_done(issue, current_user)
+ end
+
+ # When we reassign an issue we should:
+ #
+ # * create a pending todo for new assignee if issue is assigned
+ #
+ def reassigned_issue(issue, current_user)
+ create_assignment_todo(issue, current_user)
+ end
+
+ # When create a merge request we should:
+ #
+ # * creates a pending todo for assignee if merge request is assigned
+ # * create a todo for each mentioned user on merge request
+ #
+ def new_merge_request(merge_request, current_user)
+ new_issuable(merge_request, current_user)
+ end
+
+ # When update a merge request we should:
+ #
+ # * create a todo for each mentioned user on merge request
+ #
+ def update_merge_request(merge_request, current_user)
+ create_mention_todos(merge_request.project, merge_request, current_user)
+ end
+
+ # When close a merge request we should:
+ #
+ # * mark all pending todos related to the target for the current user as done
+ #
+ def close_merge_request(merge_request, current_user)
+ mark_pending_todos_as_done(merge_request, current_user)
+ end
+
+ # When we reassign a merge request we should:
+ #
+ # * creates a pending todo for new assignee if merge request is assigned
+ #
+ def reassigned_merge_request(merge_request, current_user)
+ create_assignment_todo(merge_request, current_user)
+ end
+
+ # When merge a merge request we should:
+ #
+ # * mark all pending todos related to the target for the current user as done
+ #
+ def merge_merge_request(merge_request, current_user)
+ mark_pending_todos_as_done(merge_request, current_user)
+ end
+
+ # When create a note we should:
+ #
+ # * mark all pending todos related to the noteable for the note author as done
+ # * create a todo for each mentioned user on note
+ #
+ def new_note(note, current_user)
+ handle_note(note, current_user)
+ end
+
+ # When update a note we should:
+ #
+ # * mark all pending todos related to the noteable for the current user as done
+ # * create a todo for each new user mentioned on note
+ #
+ def update_note(note, current_user)
+ handle_note(note, current_user)
+ end
+
+ # When marking pending todos as done we should:
+ #
+ # * mark all pending todos related to the target for the current user as done
+ #
+ def mark_pending_todos_as_done(target, user)
+ pending_todos(user, target.project, target).update_all(state: :done)
+ end
+
+ private
+
+ def create_todos(project, target, author, users, action, note = nil)
+ Array(users).each do |user|
+ next if pending_todos(user, project, target).exists?
+
+ Todo.create(
+ project: project,
+ user_id: user.id,
+ author_id: author.id,
+ target_id: target.id,
+ target_type: target.class.name,
+ action: action,
+ note: note
+ )
+ end
+ end
+
+ def new_issuable(issuable, author)
+ create_assignment_todo(issuable, author)
+ create_mention_todos(issuable.project, issuable, author)
+ end
+
+ def handle_note(note, author)
+ # Skip system notes, like status changes and cross-references
+ return if note.system
+
+ project = note.project
+ target = note.noteable
+
+ mark_pending_todos_as_done(target, author)
+ create_mention_todos(project, target, author, note)
+ end
+
+ def create_assignment_todo(issuable, author)
+ if issuable.assignee && issuable.assignee != author
+ create_todos(issuable.project, issuable, author, issuable.assignee, Todo::ASSIGNED)
+ end
+ end
+
+ def create_mention_todos(project, issuable, author, note = nil)
+ mentioned_users = filter_mentioned_users(project, note || issuable, author)
+ create_todos(project, issuable, author, mentioned_users, Todo::MENTIONED, note)
+ end
+
+ def filter_mentioned_users(project, target, author)
+ mentioned_users = target.mentioned_users.select do |user|
+ user.can?(:read_project, project)
+ end
+
+ mentioned_users.delete(author)
+ mentioned_users.uniq
+ end
+
+ def pending_todos(user, project, target)
+ user.todos.pending.where(
+ project_id: project.id,
+ target_id: target.id,
+ target_type: target.class.name
+ )
+ end
+end
diff --git a/app/views/dashboard/todos/_todo.html.haml b/app/views/dashboard/todos/_todo.html.haml
new file mode 100644
index 00000000000..6975f6ed0db
--- /dev/null
+++ b/app/views/dashboard/todos/_todo.html.haml
@@ -0,0 +1,21 @@
+%li{class: "todo todo-#{todo.done? ? 'done' : 'pending'}", id: dom_id(todo) }
+ .todo-item{class: 'todo-block'}
+ = image_tag avatar_icon(todo.author_email, 40), class: 'avatar s40', alt:''
+
+ .todo-title
+ %span.author_name
+ = link_to_author todo
+ %span.todo_label
+ = todo_action_name(todo)
+ = todo_target_link(todo)
+
+ &middot; #{time_ago_with_tooltip(todo.created_at)}
+
+ - if todo.pending?
+ .todo-actions.pull-right
+ = link_to 'Done', [:dashboard, todo], method: :delete, class: 'btn'
+
+ .todo-body
+ .todo-note
+ .md
+ = event_note(todo.body, project: todo.project)
diff --git a/app/views/dashboard/todos/index.html.haml b/app/views/dashboard/todos/index.html.haml
new file mode 100644
index 00000000000..946d7df3933
--- /dev/null
+++ b/app/views/dashboard/todos/index.html.haml
@@ -0,0 +1,62 @@
+- page_title "Todos"
+- header_title "Todos", dashboard_todos_path
+
+.top-area
+ %ul.nav-links
+ %li{class: ('active' if params[:state].blank? || params[:state] == 'pending')}
+ = link_to todos_filter_path(state: 'pending') do
+ %span
+ To do
+ %span{class: 'badge'}
+ = todos_pending_count
+ %li{class: ('active' if params[:state] == 'done')}
+ = link_to todos_filter_path(state: 'done') do
+ %span
+ Done
+ %span{class: 'badge'}
+ = todos_done_count
+
+ .nav-controls
+ - if @todos.any?(&:pending?)
+ = link_to 'Mark all as done', destroy_all_dashboard_todos_path(todos_filter_params), class: 'btn', method: :delete
+
+.todos-filters
+ .gray-content-block.second-block
+ = form_tag todos_filter_path(without: [:project_id, :author_id, :type, :action_id]), method: :get, class: 'filter-form' do
+ .filter-item.inline
+ = select_tag('project_id', todo_projects_options,
+ class: 'select2 trigger-submit', include_blank: true,
+ data: {placeholder: 'Project'})
+ .filter-item.inline
+ = users_select_tag(:author_id, selected: params[:author_id],
+ placeholder: 'Author', class: 'trigger-submit', any_user: "Any Author", first_user: true, current_user: true)
+ .filter-item.inline
+ = select_tag('type', todo_types_options,
+ class: 'select2 trigger-submit', include_blank: true,
+ data: {placeholder: 'Type'})
+ .filter-item.inline.actions-filter
+ = select_tag('action_id', todo_actions_options,
+ class: 'select2 trigger-submit', include_blank: true,
+ data: {placeholder: 'Action'})
+
+.prepend-top-default
+ - if @todos.any?
+ - @todos.group_by(&:project).each do |group|
+ .panel.panel-default.panel-small
+ - project = group[0]
+ .panel-heading
+ = link_to project.name_with_namespace, namespace_project_path(project.namespace, project)
+
+ %ul.well-list.todos-list
+ = render group[1]
+ = paginate @todos, theme: "gitlab"
+ - else
+ .nothing-here-block You're all done!
+
+:javascript
+ new UsersSelect();
+
+ $('form.filter-form').on('submit', function (event) {
+ event.preventDefault();
+ Turbolinks.visit(this.action + '&' + $(this).serialize());
+ });
diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml
index fcb6b835a7e..4781ff23507 100644
--- a/app/views/layouts/header/_default.html.haml
+++ b/app/views/layouts/header/_default.html.haml
@@ -21,6 +21,10 @@
%li
= link_to admin_root_path, title: 'Admin Area', data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
= icon('wrench fw')
+ %li
+ = link_to dashboard_todos_path, title: 'Todos', data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
+ %span.badge.todos-pending-count
+ = todos_pending_count
- if current_user.can_create_project?
%li
= link_to new_project_path, title: 'New project', data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
@@ -39,4 +43,4 @@
= render 'shared/outdated_browser'
- if @project && !@project.empty_repo?
:javascript
- var findFileURL = "#{namespace_project_find_file_path(@project.namespace, @project, @ref || @project.repository.root_ref)}"; \ No newline at end of file
+ var findFileURL = "#{namespace_project_find_file_path(@project.namespace, @project, @ref || @project.repository.root_ref)}";
diff --git a/app/views/layouts/nav/_dashboard.html.haml b/app/views/layouts/nav/_dashboard.html.haml
index 106abd24a56..db0cf393922 100644
--- a/app/views/layouts/nav/_dashboard.html.haml
+++ b/app/views/layouts/nav/_dashboard.html.haml
@@ -4,6 +4,12 @@
= icon('home fw')
%span
Projects
+ = nav_link(controller: :todos) do
+ = link_to dashboard_todos_path, title: 'Todos' do
+ = icon('bell fw')
+ %span
+ Todos
+ %span.count= number_with_delimiter(todos_pending_count)
= nav_link(path: 'dashboard#activity') do
= link_to activity_dashboard_path, class: 'shortcuts-activity', title: 'Activity' do
= icon('dashboard fw')
@@ -25,12 +31,12 @@
%span
Issues
%span.count= number_with_delimiter(current_user.assigned_issues.opened.count)
- = nav_link(path: 'dashboard#merge_requests') do
- = link_to assigned_mrs_dashboard_path, title: 'Merge Requests', class: 'shortcuts-merge_requests' do
- = icon('tasks fw')
- %span
- Merge Requests
- %span.count= number_with_delimiter(current_user.assigned_merge_requests.opened.count)
+ = nav_link(path: 'dashboard#merge_requests') do
+ = link_to assigned_mrs_dashboard_path, title: 'Merge Requests', class: 'shortcuts-merge_requests' do
+ = icon('tasks fw')
+ %span
+ Merge Requests
+ %span.count= number_with_delimiter(current_user.assigned_merge_requests.opened.count)
= nav_link(controller: :snippets) do
= link_to dashboard_snippets_path, title: 'Snippets' do
= icon('clipboard fw')