Commit 5ac17fb2 authored by Stan Hu's avatar Stan Hu

Merge branch 'manual-todos-issuable-sidebar' into 'master'

Manually create todo for issuable

## What does this MR do?

Adds a button to the sidebar in issues & merge requests to allow users to manually create a todo item themselves.

## What are the relevant issue numbers?

Closes #15045 

## Screenshots (if relevant)

![Screen_Shot_2016-06-07_at_09.52.14](/uploads/00af70244c0589d19f241c3e85f3d63d/Screen_Shot_2016-06-07_at_09.52.14.png)

![Screen_Shot_2016-06-07_at_09.52.06](/uploads/e232b02208613a4a50cff4d1e6f119ff/Screen_Shot_2016-06-07_at_09.52.06.png)

![Screen_Shot_2016-06-07_at_09.51.14](/uploads/f1d36435d49ab882538ae2252bec8086/Screen_Shot_2016-06-07_at_09.51.14.png)

See merge request !4502
parents 06784ee7 b22ba26c
...@@ -57,6 +57,7 @@ v 8.9.0 (unreleased) ...@@ -57,6 +57,7 @@ v 8.9.0 (unreleased)
- Use Knapsack only in CI environment - Use Knapsack only in CI environment
- Cache project build count in sidebar nav - Cache project build count in sidebar nav
- Add milestone expire date to the right sidebar - Add milestone expire date to the right sidebar
- Manually mark a issue or merge request as a todo
- Fix markdown_spec to use before instead of before(:all) to properly cleanup database after testing - Fix markdown_spec to use before instead of before(:all) to properly cleanup database after testing
- Reduce number of queries needed to render issue labels in the sidebar - Reduce number of queries needed to render issue labels in the sidebar
- Improve error handling importing projects - Improve error handling importing projects
......
...@@ -43,6 +43,55 @@ class @Sidebar ...@@ -43,6 +43,55 @@ class @Sidebar
$('.right-sidebar') $('.right-sidebar')
.hasClass('right-sidebar-collapsed'), { path: '/' }) .hasClass('right-sidebar-collapsed'), { path: '/' })
$(document)
.off 'click', '.js-issuable-todo'
.on 'click', '.js-issuable-todo', @toggleTodo
toggleTodo: (e) =>
$this = $(e.currentTarget)
$todoLoading = $('.js-issuable-todo-loading')
$btnText = $('.js-issuable-todo-text', $this)
ajaxType = if $this.attr('data-id') then 'PATCH' else 'POST'
ajaxUrlExtra = if $this.attr('data-id') then "/#{$this.attr('data-id')}" else ''
$.ajax(
url: "#{$this.data('url')}#{ajaxUrlExtra}"
type: ajaxType
dataType: 'json'
data:
issuable_id: $this.data('issuable')
issuable_type: $this.data('issuable-type')
beforeSend: =>
@beforeTodoSend($this, $todoLoading)
).done (data) =>
@todoUpdateDone(data, $this, $btnText, $todoLoading)
beforeTodoSend: ($btn, $todoLoading) ->
$btn.disable()
$todoLoading.removeClass 'hidden'
todoUpdateDone: (data, $btn, $btnText, $todoLoading) ->
$todoPendingCount = $('.todos-pending-count')
$todoPendingCount.text data.count
$btn.enable()
$todoLoading.addClass 'hidden'
if data.count is 0
$todoPendingCount.addClass 'hidden'
else
$todoPendingCount.removeClass 'hidden'
if data.todo?
$btn
.attr 'aria-label', $btn.data('mark-text')
.attr 'data-id', data.todo.id
$btnText.text $btn.data('mark-text')
else
$btn
.attr 'aria-label', $btn.data('todo-text')
.removeAttr 'data-id'
$btnText.text $btn.data('todo-text')
sidebarDropdownLoading: (e) -> sidebarDropdownLoading: (e) ->
$sidebarCollapsedIcon = $(@).closest('.block').find('.sidebar-collapsed-icon') $sidebarCollapsedIcon = $(@).closest('.block').find('.sidebar-collapsed-icon')
...@@ -117,5 +166,3 @@ class @Sidebar ...@@ -117,5 +166,3 @@ class @Sidebar
getBlock: (name) -> getBlock: (name) ->
@sidebar.find(".block.#{name}") @sidebar.find(".block.#{name}")
...@@ -34,6 +34,10 @@ ...@@ -34,6 +34,10 @@
color: inherit; color: inherit;
} }
.issuable-header-text {
margin-top: 7px;
}
.block { .block {
@include clearfix; @include clearfix;
padding: $gl-padding 0; padding: $gl-padding 0;
...@@ -60,10 +64,6 @@ ...@@ -60,10 +64,6 @@
margin-top: 0; margin-top: 0;
} }
.issuable-count {
margin-top: 7px;
}
.gutter-toggle { .gutter-toggle {
margin-left: 20px; margin-left: 20px;
padding-left: 10px; padding-left: 10px;
...@@ -250,7 +250,7 @@ ...@@ -250,7 +250,7 @@
} }
} }
.issuable-pager { .issuable-header-btn {
background: $gray-normal; background: $gray-normal;
border: 1px solid $border-gray-normal; border: 1px solid $border-gray-normal;
&:hover { &:hover {
...@@ -263,7 +263,7 @@ ...@@ -263,7 +263,7 @@
} }
} }
a:not(.issuable-pager) { a {
&:hover { &:hover {
color: $md-link-color; color: $md-link-color;
text-decoration: none; text-decoration: none;
......
class Projects::TodosController < Projects::ApplicationController
def create
todos = TodoService.new.mark_todo(issuable, current_user)
render json: {
todo: todos,
count: current_user.todos.pending.count,
}
end
def update
current_user.todos.find_by_id(params[:id]).update(state: :done)
render json: {
count: current_user.todos.pending.count,
}
end
private
def issuable
@issuable ||= begin
case params[:issuable_type]
when "issue"
@project.issues.find(params[:issuable_id])
when "merge_request"
@project.merge_requests.find(params[:issuable_id])
end
end
end
end
...@@ -36,7 +36,7 @@ class TodosFinder ...@@ -36,7 +36,7 @@ class TodosFinder
private private
def action_id? 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 end
def action_id def action_id
......
...@@ -67,6 +67,12 @@ module IssuablesHelper ...@@ -67,6 +67,12 @@ module IssuablesHelper
end end
end end
def has_todo(issuable)
unless current_user.nil?
current_user.todos.find_by(target_id: issuable.id, state: :pending)
end
end
private private
def sidebar_gutter_collapsed? def sidebar_gutter_collapsed?
......
...@@ -12,6 +12,7 @@ module TodosHelper ...@@ -12,6 +12,7 @@ module TodosHelper
when Todo::ASSIGNED then 'assigned you' when Todo::ASSIGNED then 'assigned you'
when Todo::MENTIONED then 'mentioned you on' when Todo::MENTIONED then 'mentioned you on'
when Todo::BUILD_FAILED then 'The build failed for your' when Todo::BUILD_FAILED then 'The build failed for your'
when Todo::MARKED then 'marked this as a Todo for'
end end
end end
......
...@@ -2,6 +2,7 @@ class Todo < ActiveRecord::Base ...@@ -2,6 +2,7 @@ class Todo < ActiveRecord::Base
ASSIGNED = 1 ASSIGNED = 1
MENTIONED = 2 MENTIONED = 2
BUILD_FAILED = 3 BUILD_FAILED = 3
MARKED = 4
belongs_to :author, class_name: "User" belongs_to :author, class_name: "User"
belongs_to :note belongs_to :note
......
...@@ -139,10 +139,16 @@ class TodoService ...@@ -139,10 +139,16 @@ class TodoService
pending_todos(user, attributes).update_all(state: :done) pending_todos(user, attributes).update_all(state: :done)
end 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 private
def create_todos(users, attributes) def create_todos(users, attributes)
Array(users).each do |user| Array(users).map do |user|
next if pending_todos(user, attributes).exists? next if pending_todos(user, attributes).exists?
Todo.create(attributes.merge(user_id: user.id)) Todo.create(attributes.merge(user_id: user.id))
end end
......
...@@ -27,8 +27,7 @@ ...@@ -27,8 +27,7 @@
%li %li
= link_to dashboard_todos_path, title: 'Todos', data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do = link_to dashboard_todos_path, title: 'Todos', data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
= icon('bell fw') = icon('bell fw')
- unless todos_pending_count == 0 %span.badge.todos-pending-count{ class: ("hidden" if todos_pending_count == 0) }
%span.badge.todos-pending-count
= todos_pending_count = todos_pending_count
- if current_user.can_create_project? - if current_user.can_create_project?
%li %li
......
- todo = has_todo(issuable)
%aside.right-sidebar{ class: sidebar_gutter_collapsed_class } %aside.right-sidebar{ class: sidebar_gutter_collapsed_class }
.issuable-sidebar .issuable-sidebar
- can_edit_issuable = can?(current_user, :"admin_#{issuable.to_ability_name}", @project) - can_edit_issuable = can?(current_user, :"admin_#{issuable.to_ability_name}", @project)
.block.issuable-sidebar-header .block.issuable-sidebar-header
%a.gutter-toggle.pull-right.js-sidebar-toggle{href: '#'} - if current_user
%span.issuable-header-text.hide-collapsed.pull-left
Todo
%a.gutter-toggle.pull-right.js-sidebar-toggle{ role: "button", href: "#", aria: { label: "Toggle sidebar" } }
= sidebar_gutter_toggle_icon = sidebar_gutter_toggle_icon
- if current_user
%button.btn.btn-default.issuable-header-btn.pull-right.js-issuable-todo{ type: "button", aria: { label: (todo.nil? ? "Add Todo" : "Mark Done") }, data: { todo_text: "Add Todo", mark_text: "Mark Done", id: (todo.id unless todo.nil?), issuable: issuable.id, issuable_type: issuable.class.name.underscore, url: namespace_project_todos_path(@project.namespace, @project) } }
%span.js-issuable-todo-text
- if todo.nil?
Add Todo
- else
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| = 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 .block.assignee
......
...@@ -795,6 +795,8 @@ Rails.application.routes.draw do ...@@ -795,6 +795,8 @@ Rails.application.routes.draw do
end end
end end
resources :todos, only: [:create, :update], constraints: { id: /\d+/ }
resources :uploads, only: [:create] do resources :uploads, only: [:create] do
collection do collection do
get ":secret/:filename", action: :show, as: :show, constraints: { filename: /[^\/]+/ } get ":secret/:filename", action: :show, as: :show, constraints: { filename: /[^\/]+/ }
......
require 'rails_helper'
feature 'Manually create a todo item from issue', feature: true, js: true do
let!(:project) { create(:project) }
let!(:issue) { create(:issue, project: project) }
let!(:user) { create(:user)}
before do
project.team << [user, :master]
login_as(user)
visit namespace_project_issue_path(project.namespace, project, issue)
end
it 'should create todo when clicking button' do
page.within '.issuable-sidebar' do
click_button 'Add Todo'
expect(page).to have_content 'Mark Done'
end
page.within '.header-content .todos-pending-count' do
expect(page).to have_content '1'
end
end
it 'should mark a todo as done' do
page.within '.issuable-sidebar' do
click_button 'Add Todo'
click_button 'Mark Done'
end
expect(page).to have_selector('.todos-pending-count', visible: false)
end
end
...@@ -228,6 +228,14 @@ describe TodoService, services: true do ...@@ -228,6 +228,14 @@ describe TodoService, services: true do
should_not_create_any_todo { service.new_note(note_on_project_snippet, john_doe) } should_not_create_any_todo { service.new_note(note_on_project_snippet, john_doe) }
end end
end end
describe '#mark_todo' do
it 'creates a todo from a issue' do
service.mark_todo(unassigned_issue, author)
should_create_todo(user: author, target: unassigned_issue, action: Todo::MARKED)
end
end
end end
describe 'Merge Requests' do describe 'Merge Requests' do
...@@ -361,6 +369,14 @@ describe TodoService, services: true do ...@@ -361,6 +369,14 @@ describe TodoService, services: true do
expect(second_todo.reload).not_to be_done expect(second_todo.reload).not_to be_done
end end
end end
describe '#mark_todo' do
it 'creates a todo from a merge request' do
service.mark_todo(mr_unassigned, author)
should_create_todo(user: author, target: mr_unassigned, action: Todo::MARKED)
end
end
end end
def should_create_todo(attributes = {}) def should_create_todo(attributes = {})
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment