From f67b06ada016915211e84a7d12a063aa25e422f3 Mon Sep 17 00:00:00 2001
From: Phil Hughes <me@iamphill.com>
Date: Tue, 7 Jun 2016 09:44:01 +0100
Subject: [PATCH] Manually create todo for issuable Added a button into the
 sidebar for issues & merge requests to allow users to manually create todo
 items

Closes #15045
---
 .../javascripts/right_sidebar.js.coffee       | 41 ++++++++++++++++++-
 app/assets/stylesheets/pages/issuable.scss    | 12 +++---
 app/controllers/projects/issues_controller.rb | 14 +++++++
 .../projects/merge_requests_controller.rb     | 14 +++++++
 app/finders/todos_finder.rb                   |  2 +-
 app/helpers/issuables_helper.rb               | 14 +++++++
 app/models/todo.rb                            |  1 +
 app/services/todo_service.rb                  |  6 +++
 app/views/layouts/header/_default.html.haml   |  5 +--
 app/views/shared/issuable/_sidebar.html.haml  | 13 +++++-
 config/routes.rb                              |  2 +
 11 files changed, 111 insertions(+), 13 deletions(-)

diff --git a/app/assets/javascripts/right_sidebar.js.coffee b/app/assets/javascripts/right_sidebar.js.coffee
index c9cb0f4bb32..3ee943fe78c 100644
--- a/app/assets/javascripts/right_sidebar.js.coffee
+++ b/app/assets/javascripts/right_sidebar.js.coffee
@@ -43,6 +43,45 @@ class @Sidebar
             $('.right-sidebar')
               .hasClass('right-sidebar-collapsed'), { path: '/' })
 
+    $(document)
+      .off 'click', '.js-issuable-todo'
+      .on 'click', '.js-issuable-todo', @toggleTodo
+
+  toggleTodo: (e) ->
+    $this = $(@)
+    $btnText = $this.find('span')
+    data = {
+      todo_id: $this.attr('data-id')
+    }
+
+    $.ajax(
+      url: $this.data('url')
+      type: 'POST'
+      dataType: 'json'
+      data: data
+      beforeSend: ->
+        $this.disable()
+        $('.js-issuable-todo-loading').removeClass 'hidden'
+    ).done (data) ->
+      $todoPendingCount = $('.todos-pending-count')
+      $todoPendingCount.text data.count
+
+      $this.enable()
+      $('.js-issuable-todo-loading').addClass 'hidden'
+
+      if data.count is 0
+        $this.removeAttr 'data-id'
+        $btnText.text $this.data('todo-text')
+
+        $todoPendingCount
+          .addClass 'hidden'
+      else
+        $btnText.text $this.data('mark-text')
+        $todoPendingCount
+          .removeClass 'hidden'
+
+      if data.todo?
+        $this.attr 'data-id', data.todo.id
 
   sidebarDropdownLoading: (e) ->
     $sidebarCollapsedIcon = $(@).closest('.block').find('.sidebar-collapsed-icon')
@@ -117,5 +156,3 @@ class @Sidebar
 
   getBlock: (name) ->
     @sidebar.find(".block.#{name}")
-
-
diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss
index ea453ce356a..acbb7e7f713 100644
--- a/app/assets/stylesheets/pages/issuable.scss
+++ b/app/assets/stylesheets/pages/issuable.scss
@@ -34,6 +34,10 @@
     color: inherit;
   }
 
+  .issuable-header-text {
+    margin-top: 7px;
+  }
+
   .block {
     @include clearfix;
     padding: $gl-padding 0;
@@ -60,10 +64,6 @@
       margin-top: 0;
     }
 
-    .issuable-count {
-      margin-top: 7px;
-    }
-
     .gutter-toggle {
       margin-left: 20px;
       padding-left: 10px;
@@ -250,7 +250,7 @@
     }
   }
 
-  .issuable-pager {
+  .issuable-header-btn {
     background: $gray-normal;
     border: 1px solid $border-gray-normal;
     &:hover {
@@ -263,7 +263,7 @@
     }
   }
 
-  a:not(.issuable-pager) {
+  a:not(.issuable-header-btn) {
     &:hover {
       color: $md-link-color;
       text-decoration: none;
diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb
index 4e2d3bebb2e..5678d584d4a 100644
--- a/app/controllers/projects/issues_controller.rb
+++ b/app/controllers/projects/issues_controller.rb
@@ -164,6 +164,20 @@ class Projects::IssuesController < Projects::ApplicationController
     end
   end
 
+  def todo
+    json_data = Hash.new
+
+    if params[:todo_id].nil?
+      TodoService.new.mark_todo(issue, current_user)
+
+      json_data[:todo] = current_user.todos.find_by(state: :pending, action: Todo::MARKED, target_id: issue.id)
+    else
+      current_user.todos.find_by_id(params[:todo_id]).update(state: :done)
+    end
+
+    render json: json_data.merge({ count: current_user.todos.pending.count })
+  end
+
   protected
 
   def issue
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index 67e7187c10d..f0eba453caa 100644
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -260,6 +260,20 @@ class Projects::MergeRequestsController < Projects::ApplicationController
     render json: response
   end
 
+  def todo
+    json_data = Hash.new
+
+    if params[:todo_id].nil?
+      TodoService.new.mark_todo(merge_request, current_user)
+
+      json_data[:todo] = current_user.todos.find_by(state: :pending, action: Todo::MARKED, target_id: merge_request.id)
+    else
+      current_user.todos.find_by_id(params[:todo_id]).update(state: :done)
+    end
+
+    render json: json_data.merge({ count: current_user.todos.pending.count })
+  end
+
   protected
 
   def selected_target_project
diff --git a/app/finders/todos_finder.rb b/app/finders/todos_finder.rb
index 1d88116d7d2..aa47c6c157e 100644
--- a/app/finders/todos_finder.rb
+++ b/app/finders/todos_finder.rb
@@ -36,7 +36,7 @@ class TodosFinder
   private
 
   def action_id?
-    action_id.present? && [Todo::ASSIGNED, Todo::MENTIONED, Todo::BUILD_FAILED].include?(action_id.to_i)
+    action_id.present? && [Todo::ASSIGNED, Todo::MENTIONED, Todo::BUILD_FAILED, Todo::MARKED].include?(action_id.to_i)
   end
 
   def action_id
diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb
index 40d8ce8a1d3..88ef1a6468c 100644
--- a/app/helpers/issuables_helper.rb
+++ b/app/helpers/issuables_helper.rb
@@ -67,6 +67,20 @@ module IssuablesHelper
     end
   end
 
+  def issuable_todo_path(issuable)
+    project = issuable.project
+
+    if issuable.kind_of?(MergeRequest)
+      todo_namespace_project_merge_request_path(project.namespace, project, issuable.iid, :json)
+    else
+      todo_namespace_project_issue_path(project.namespace, project, issuable.iid, :json)
+    end
+  end
+
+  def has_todo(issuable)
+    current_user.todos.find_by(target_id: issuable.id, state: :pending)
+  end
+
   private
 
   def sidebar_gutter_collapsed?
diff --git a/app/models/todo.rb b/app/models/todo.rb
index 3a091373329..2792fa9b9a8 100644
--- a/app/models/todo.rb
+++ b/app/models/todo.rb
@@ -2,6 +2,7 @@ class Todo < ActiveRecord::Base
   ASSIGNED     = 1
   MENTIONED    = 2
   BUILD_FAILED = 3
+  MARKED       = 4
 
   belongs_to :author, class_name: "User"
   belongs_to :note
diff --git a/app/services/todo_service.rb b/app/services/todo_service.rb
index 8e03ff8ddde..5a192e54f25 100644
--- a/app/services/todo_service.rb
+++ b/app/services/todo_service.rb
@@ -139,6 +139,12 @@ class TodoService
     pending_todos(user, attributes).update_all(state: :done)
   end
 
+  # When user marks an issue as todo
+  def mark_todo(issuable, current_user)
+    attributes = attributes_for_todo(issuable.project, issuable, current_user, Todo::MARKED)
+    create_todos(current_user, attributes)
+  end
+
   private
 
   def create_todos(users, attributes)
diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml
index ad30a367fc5..ebc9f01675a 100644
--- a/app/views/layouts/header/_default.html.haml
+++ b/app/views/layouts/header/_default.html.haml
@@ -27,9 +27,8 @@
             %li
               = link_to dashboard_todos_path, title: 'Todos', data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
                 = icon('bell fw')
-                - unless todos_pending_count == 0
-                  %span.badge.todos-pending-count
-                    = todos_pending_count
+                %span.badge.todos-pending-count{ class: ("hidden" if todos_pending_count == 0)}
+                  = 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
diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml
index fb906de829a..25d830b6e49 100644
--- a/app/views/shared/issuable/_sidebar.html.haml
+++ b/app/views/shared/issuable/_sidebar.html.haml
@@ -1,9 +1,20 @@
+- todo = has_todo(issuable)
 %aside.right-sidebar{ class: sidebar_gutter_collapsed_class }
   .issuable-sidebar
     - can_edit_issuable = can?(current_user, :"admin_#{issuable.to_ability_name}", @project)
     .block.issuable-sidebar-header
-      %a.gutter-toggle.pull-right.js-sidebar-toggle{href: '#'}
+      %span.issuable-header-text.hide-collapsed.pull-left
+        Todo
+      %button.gutter-toggle.pull-right.js-sidebar-toggle{ type: "button", aria: { label: "Toggle sidebar" } }
         = sidebar_gutter_toggle_icon
+      %button.btn.btn-default.issuable-header-btn.pull-right.js-issuable-todo{ type: "button", data: { todo_text: "Add Todo", mark_text: "Mark Done", id: (todo.id unless todo.nil?), url: issuable_todo_path(issuable) } }
+        - if todo.nil?
+          %span
+            Add Todo
+        - else
+          %span
+            Mark Done
+        = icon('spin spinner', class: 'hidden js-issuable-todo-loading')
 
     = form_for [@project.namespace.becomes(Namespace), @project, issuable], remote: true, format: :json, html: {class: 'issuable-context-form inline-update js-issuable-update'} do |f|
       .block.assignee
diff --git a/config/routes.rb b/config/routes.rb
index 95fbe7dd9df..d018fa742cc 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -679,6 +679,7 @@ Rails.application.routes.draw do
             post :toggle_subscription
             post :toggle_award_emoji
             post :remove_wip
+            post :todo
           end
 
           collection do
@@ -759,6 +760,7 @@ Rails.application.routes.draw do
             get :referenced_merge_requests
             get :related_branches
             get :can_create_branch
+            post :todo
           end
           collection do
             post  :bulk_update
-- 
2.30.9