Merge branch '27114-add-undo-mark-all-as-done-to-todos' into 'master'

Add 'Undo mark all as done' to Todos

Closes #27114

See merge request !9890
This commit is contained in:
Filipa Lacerda 2017-03-16 11:29:10 +00:00
commit 774e0e3bfe
12 changed files with 166 additions and 55 deletions

View File

@ -5,6 +5,7 @@ class Todos {
constructor() {
this.initFilters();
this.bindEvents();
this.todo_ids = [];
this.cleanupWrapper = this.cleanup.bind(this);
document.addEventListener('beforeunload', this.cleanupWrapper);
@ -17,16 +18,16 @@ class Todos {
unbindEvents() {
$('.js-done-todo, .js-undo-todo, .js-add-todo').off('click', this.updateRowStateClickedWrapper);
$('.js-todos-mark-all').off('click', this.allDoneClickedWrapper);
$('.js-todos-mark-all', '.js-todos-undo-all').off('click', this.updateallStateClickedWrapper);
$('.todo').off('click', this.goToTodoUrl);
}
bindEvents() {
this.updateRowStateClickedWrapper = this.updateRowStateClicked.bind(this);
this.allDoneClickedWrapper = this.allDoneClicked.bind(this);
this.updateAllStateClickedWrapper = this.updateAllStateClicked.bind(this);
$('.js-done-todo, .js-undo-todo, .js-add-todo').on('click', this.updateRowStateClickedWrapper);
$('.js-todos-mark-all').on('click', this.allDoneClickedWrapper);
$('.js-todos-mark-all, .js-todos-undo-all').on('click', this.updateAllStateClickedWrapper);
$('.todo').on('click', this.goToTodoUrl);
}
@ -57,14 +58,14 @@ class Todos {
e.preventDefault();
const target = e.target;
target.setAttribute('disabled', '');
target.setAttribute('disabled', true);
target.classList.add('disabled');
$.ajax({
type: 'POST',
url: target.getAttribute('href'),
url: target.dataset.href,
dataType: 'json',
data: {
'_method': target.getAttribute('data-method'),
'_method': target.dataset.method,
},
success: (data) => {
this.updateRowState(target);
@ -73,25 +74,6 @@ class Todos {
});
}
allDoneClicked(e) {
e.preventDefault();
const $target = $(e.currentTarget);
$target.disable();
$.ajax({
type: 'POST',
url: $target.attr('href'),
dataType: 'json',
data: {
'_method': 'delete',
},
success: (data) => {
$target.remove();
$('.js-todos-all').html('<div class="nothing-here-block">You\'re all done!</div>');
this.updateBadges(data);
},
});
}
updateRowState(target) {
const row = target.closest('li');
const restoreBtn = row.querySelector('.js-undo-todo');
@ -112,6 +94,41 @@ class Todos {
}
}
updateAllStateClicked(e) {
e.preventDefault();
const target = e.currentTarget;
const requestData = { '_method': target.dataset.method, ids: this.todo_ids };
target.setAttribute('disabled', true);
target.classList.add('disabled');
$.ajax({
type: 'POST',
url: target.dataset.href,
dataType: 'json',
data: requestData,
success: (data) => {
this.updateAllState(target, data);
return this.updateBadges(data);
},
});
}
updateAllState(target, data) {
const markAllDoneBtn = document.querySelector('.js-todos-mark-all');
const undoAllBtn = document.querySelector('.js-todos-undo-all');
const todoListContainer = document.querySelector('.js-todos-list-container');
const nothingHereContainer = document.querySelector('.js-nothing-here-container');
target.removeAttribute('disabled');
target.classList.remove('disabled');
this.todo_ids = (target === markAllDoneBtn) ? data.updated_ids : [];
undoAllBtn.classList.toggle('hidden');
markAllDoneBtn.classList.toggle('hidden');
todoListContainer.classList.toggle('hidden');
nothingHereContainer.classList.toggle('hidden');
}
updateBadges(data) {
$(document).trigger('todo:toggle', data.count);
document.querySelector('.todos-pending .badge').innerHTML = data.count;

View File

@ -22,12 +22,12 @@ class Dashboard::TodosController < Dashboard::ApplicationController
end
def destroy_all
TodoService.new.mark_todos_as_done(@todos, current_user)
updated_ids = TodoService.new.mark_todos_as_done(@todos, current_user)
respond_to do |format|
format.html { redirect_to dashboard_todos_path, notice: 'All todos were marked as done.' }
format.js { head :ok }
format.json { render json: todos_counts }
format.json { render json: todos_counts.merge(updated_ids: updated_ids) }
end
end
@ -37,6 +37,12 @@ class Dashboard::TodosController < Dashboard::ApplicationController
render json: todos_counts
end
def bulk_restore
TodoService.new.mark_todos_as_pending_by_ids(params[:ids], current_user)
render json: todos_counts
end
# Used in TodosHelper also
def self.todos_count_format(count)
count >= 100 ? '99+' : count

View File

@ -201,10 +201,12 @@ class TodoService
def update_todos_state_by_ids(ids, current_user, state)
todos = current_user.todos.where(id: ids)
# Only return those that are not really on that state
marked_todos = todos.where.not(state: state).update_all(state: state)
# Only update those that are not really on that state
todos = todos.where.not(state: state)
todos_ids = todos.pluck(:id)
todos.update_all(state: state)
current_user.update_todos_count_cache
marked_todos
todos_ids
end
def create_todos(users, attributes)

View File

@ -36,14 +36,14 @@
- if todo.pending?
.todo-actions
= link_to [:dashboard, todo], method: :delete, class: 'btn btn-loading js-done-todo' do
= link_to dashboard_todo_path(todo), method: :delete, class: 'btn btn-loading js-done-todo', data: { href: dashboard_todo_path(todo) } do
Done
= icon('spinner spin')
= link_to restore_dashboard_todo_path(todo), method: :patch, class: 'btn btn-loading js-undo-todo hidden' do
= link_to restore_dashboard_todo_path(todo), method: :patch, class: 'btn btn-loading js-undo-todo hidden', data: { href: restore_dashboard_todo_path(todo) } do
Undo
= icon('spinner spin')
- else
.todo-actions
= link_to restore_dashboard_todo_path(todo), method: :patch, class: 'btn btn-loading js-add-todo' do
= link_to restore_dashboard_todo_path(todo), method: :patch, class: 'btn btn-loading js-add-todo', data: { href: restore_dashboard_todo_path(todo) } do
Add todo
= icon('spinner spin')

View File

@ -19,9 +19,12 @@
.nav-controls
- if @todos.any?(&:pending?)
= link_to destroy_all_dashboard_todos_path(todos_filter_params), class: 'btn btn-loading js-todos-mark-all', method: :delete do
= link_to destroy_all_dashboard_todos_path(todos_filter_params), class: 'btn btn-loading js-todos-mark-all', method: :delete, data: { href: destroy_all_dashboard_todos_path(todos_filter_params) } do
Mark all as done
= icon('spinner spin')
= link_to bulk_restore_dashboard_todos_path, class: 'btn btn-loading js-todos-undo-all hidden', method: :patch , data: { href: bulk_restore_dashboard_todos_path(todos_filter_params) } do
Undo mark all as done
= icon('spinner spin')
.todos-filters
.row-content-block.second-block
@ -67,12 +70,16 @@
.js-todos-all
- if @todos.any?
.js-todos-options{ data: {per_page: @todos.limit_value, current_page: @todos.current_page, total_pages: @todos.total_pages} }
.panel.panel-default.panel-small.panel-without-border
%ul.content-list.todos-list
= render @todos
= paginate @todos, theme: "gitlab"
.js-todos-list-container
.js-todos-options{ data: { per_page: @todos.limit_value, current_page: @todos.current_page, total_pages: @todos.total_pages } }
.panel.panel-default.panel-small.panel-without-border
%ul.content-list.todos-list
= render @todos
= paginate @todos, theme: "gitlab"
.js-nothing-here-container.todos-all-done.hidden
= render "shared/empty_states/icons/todos_all_done.svg"
%h4.text-center
You're all done!
- elsif current_user.todos.any?
.todos-all-done
= render "shared/empty_states/icons/todos_all_done.svg"

View File

@ -0,0 +1,4 @@
---
title: Add Undo mark all as done to Todos
merge_request: 9890
author: Jacopo Beschi @jacopo-beschi

View File

@ -13,6 +13,7 @@ resource :dashboard, controller: 'dashboard', only: [] do
resources :todos, only: [:index, :destroy] do
collection do
delete :destroy_all
patch :bulk_restore
end
member do
patch :restore

View File

@ -159,7 +159,11 @@ class Spinach::Features::DashboardTodos < Spinach::FeatureSteps
end
def should_not_see_todo(title)
expect(page).not_to have_content title
expect(page).not_to have_visible_content title
end
def have_visible_content(text)
have_css('*', text: text, visible: true)
end
def john_doe

View File

@ -20,9 +20,9 @@ module API
desc 'Mark all todos as done'
delete do
status(200)
todos = TodosFinder.new(current_user, params).execute
TodoService.new.mark_todos_as_done(todos, current_user)
TodoService.new.mark_todos_as_done(todos, current_user).size
end
end
end

View File

@ -49,4 +49,18 @@ describe Dashboard::TodosController do
expect(json_response).to eq({ "count" => "1", "done_count" => "0" })
end
end
describe 'PATCH #bulk_restore' do
let(:todos) { create_list(:todo, 2, :done, user: user, project: project, author: author) }
it 'restores the todos to pending state' do
patch :bulk_restore, ids: todos.map(&:id)
todos.each do |todo|
expect(todo.reload).to be_pending
end
expect(response).to have_http_status(200)
expect(json_response).to eq({ 'count' => '2', 'done_count' => '0' })
end
end
end

View File

@ -31,7 +31,7 @@ describe 'Dashboard Todos', feature: true do
end
it 'shows due date as today' do
page.within first('.todo') do
within first('.todo') do
expect(page).to have_content 'Due today'
end
end
@ -184,6 +184,60 @@ describe 'Dashboard Todos', feature: true do
expect(page).to have_content "You're all done!"
expect(page).not_to have_selector('.gl-pagination')
end
it 'shows "Undo mark all as done" button' do
expect(page).to have_selector('.js-todos-mark-all', visible: false)
expect(page).to have_selector('.js-todos-undo-all', visible: true)
end
end
describe 'undo mark all as done', js: true do
before do
visit dashboard_todos_path
end
it 'shows the restored todo list' do
mark_all_and_undo
expect(page).to have_selector('.todos-list .todo', count: 1)
expect(page).to have_selector('.gl-pagination')
expect(page).not_to have_content "You're all done!"
end
it 'updates todo count' do
mark_all_and_undo
expect(page).to have_content 'To do 2'
expect(page).to have_content 'Done 0'
end
it 'shows "Mark all as done" button' do
mark_all_and_undo
expect(page).to have_selector('.js-todos-mark-all', visible: true)
expect(page).to have_selector('.js-todos-undo-all', visible: false)
end
context 'User has deleted a todo' do
before do
within first('.todo') do
click_link 'Done'
end
end
it 'shows the restored todo list with the deleted todo' do
mark_all_and_undo
expect(page).to have_selector('.todos-list .todo.todo-pending', count: 1)
end
end
def mark_all_and_undo
click_link 'Mark all as done'
wait_for_ajax
click_link 'Undo mark all as done'
wait_for_ajax
end
end
end

View File

@ -298,6 +298,10 @@ describe TodoService, services: true do
expect(second_todo.reload.state?(new_state)).to be true
end
it 'returns the updated ids' do
expect(service.send(meth, collection, john_doe)).to match_array([first_todo.id, second_todo.id])
end
describe 'cached counts' do
it 'updates when todos change' do
expect(john_doe.todos.where(state: new_state).count).to eq(0)
@ -706,7 +710,7 @@ describe TodoService, services: true do
should_create_todo(user: admin, author: admin, target: mr_unassigned, action: Todo::UNMERGEABLE)
end
end
describe '#mark_todo' do
it 'creates a todo from a merge request' do
service.mark_todo(mr_unassigned, author)
@ -779,29 +783,27 @@ describe TodoService, services: true do
.to change { todo.reload.state }.from('pending').to('done')
end
it 'returns the number of updated todos' do # Needed on API
it 'returns the ids of updated todos' do # Needed on API
todo = create(:todo, :mentioned, user: john_doe, target: issue, project: project)
expect(TodoService.new.mark_todos_as_done([todo], john_doe)).to eq(1)
expect(TodoService.new.mark_todos_as_done([todo], john_doe)).to eq([todo.id])
end
context 'when some of the todos are done already' do
before do
create(:todo, :mentioned, user: john_doe, target: issue, project: project)
create(:todo, :mentioned, user: john_doe, target: another_issue, project: project)
end
let!(:first_todo) { create(:todo, :mentioned, user: john_doe, target: issue, project: project) }
let!(:second_todo) { create(:todo, :mentioned, user: john_doe, target: another_issue, project: project) }
it 'returns the number of those still pending' do
it 'returns the ids of those still pending' do
TodoService.new.mark_pending_todos_as_done(issue, john_doe)
expect(TodoService.new.mark_todos_as_done(Todo.all, john_doe)).to eq(1)
expect(TodoService.new.mark_todos_as_done(Todo.all, john_doe)).to eq([second_todo.id])
end
it 'returns 0 if all are done' do
it 'returns an empty array if all are done' do
TodoService.new.mark_pending_todos_as_done(issue, john_doe)
TodoService.new.mark_pending_todos_as_done(another_issue, john_doe)
expect(TodoService.new.mark_todos_as_done(Todo.all, john_doe)).to eq(0)
expect(TodoService.new.mark_todos_as_done(Todo.all, john_doe)).to eq([])
end
end