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
This commit is contained in:
commit
5ac17fb2fa
|
@ -57,6 +57,7 @@ v 8.9.0 (unreleased)
|
|||
- Use Knapsack only in CI environment
|
||||
- Cache project build count in sidebar nav
|
||||
- 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
|
||||
- Reduce number of queries needed to render issue labels in the sidebar
|
||||
- Improve error handling importing projects
|
||||
|
|
|
@ -43,6 +43,55 @@ class @Sidebar
|
|||
$('.right-sidebar')
|
||||
.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) ->
|
||||
$sidebarCollapsedIcon = $(@).closest('.block').find('.sidebar-collapsed-icon')
|
||||
|
@ -117,5 +166,3 @@ class @Sidebar
|
|||
|
||||
getBlock: (name) ->
|
||||
@sidebar.find(".block.#{name}")
|
||||
|
||||
|
||||
|
|
|
@ -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 {
|
||||
&:hover {
|
||||
color: $md-link-color;
|
||||
text-decoration: none;
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
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
|
|||
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
|
||||
|
|
|
@ -67,6 +67,12 @@ module IssuablesHelper
|
|||
end
|
||||
end
|
||||
|
||||
def has_todo(issuable)
|
||||
unless current_user.nil?
|
||||
current_user.todos.find_by(target_id: issuable.id, state: :pending)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def sidebar_gutter_collapsed?
|
||||
|
|
|
@ -12,6 +12,7 @@ module TodosHelper
|
|||
when Todo::ASSIGNED then 'assigned you'
|
||||
when Todo::MENTIONED then 'mentioned you on'
|
||||
when Todo::BUILD_FAILED then 'The build failed for your'
|
||||
when Todo::MARKED then 'marked this as a Todo for'
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -139,10 +139,16 @@ 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)
|
||||
Array(users).each do |user|
|
||||
Array(users).map do |user|
|
||||
next if pending_todos(user, attributes).exists?
|
||||
Todo.create(attributes.merge(user_id: user.id))
|
||||
end
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,9 +1,21 @@
|
|||
- 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: '#'}
|
||||
- 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
|
||||
- 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|
|
||||
.block.assignee
|
||||
|
|
|
@ -795,6 +795,8 @@ Rails.application.routes.draw do
|
|||
end
|
||||
end
|
||||
|
||||
resources :todos, only: [:create, :update], constraints: { id: /\d+/ }
|
||||
|
||||
resources :uploads, only: [:create] do
|
||||
collection do
|
||||
get ":secret/:filename", action: :show, as: :show, constraints: { filename: /[^\/]+/ }
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
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
|
|||
should_not_create_any_todo { service.new_note(note_on_project_snippet, john_doe) }
|
||||
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
|
||||
|
||||
describe 'Merge Requests' do
|
||||
|
@ -361,6 +369,14 @@ describe TodoService, services: true do
|
|||
expect(second_todo.reload).not_to be_done
|
||||
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
|
||||
|
||||
def should_create_todo(attributes = {})
|
||||
|
|
Loading…
Reference in New Issue