Add 'Undo mark all as done' to Todos

Added the ability to 'Undo mark all as done' todos marked as complete with 'Mark all as done'
in the 'Todo' tab of the Todo dashboard.
The operation undos only the todo previously marked as done with the 'Mark al as done' button.
This commit is contained in:
Jacopo 2017-01-29 10:44:30 +01:00
parent 5e05d6b8cf
commit 5f9ace8eb1
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