Commit 774e0e3b authored by Filipa Lacerda's avatar Filipa Lacerda

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
parents 9ce549b2 5f9ace8e
...@@ -5,6 +5,7 @@ class Todos { ...@@ -5,6 +5,7 @@ class Todos {
constructor() { constructor() {
this.initFilters(); this.initFilters();
this.bindEvents(); this.bindEvents();
this.todo_ids = [];
this.cleanupWrapper = this.cleanup.bind(this); this.cleanupWrapper = this.cleanup.bind(this);
document.addEventListener('beforeunload', this.cleanupWrapper); document.addEventListener('beforeunload', this.cleanupWrapper);
...@@ -17,16 +18,16 @@ class Todos { ...@@ -17,16 +18,16 @@ class Todos {
unbindEvents() { unbindEvents() {
$('.js-done-todo, .js-undo-todo, .js-add-todo').off('click', this.updateRowStateClickedWrapper); $('.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); $('.todo').off('click', this.goToTodoUrl);
} }
bindEvents() { bindEvents() {
this.updateRowStateClickedWrapper = this.updateRowStateClicked.bind(this); 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-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); $('.todo').on('click', this.goToTodoUrl);
} }
...@@ -57,14 +58,14 @@ class Todos { ...@@ -57,14 +58,14 @@ class Todos {
e.preventDefault(); e.preventDefault();
const target = e.target; const target = e.target;
target.setAttribute('disabled', ''); target.setAttribute('disabled', true);
target.classList.add('disabled'); target.classList.add('disabled');
$.ajax({ $.ajax({
type: 'POST', type: 'POST',
url: target.getAttribute('href'), url: target.dataset.href,
dataType: 'json', dataType: 'json',
data: { data: {
'_method': target.getAttribute('data-method'), '_method': target.dataset.method,
}, },
success: (data) => { success: (data) => {
this.updateRowState(target); this.updateRowState(target);
...@@ -73,25 +74,6 @@ class Todos { ...@@ -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) { updateRowState(target) {
const row = target.closest('li'); const row = target.closest('li');
const restoreBtn = row.querySelector('.js-undo-todo'); const restoreBtn = row.querySelector('.js-undo-todo');
...@@ -112,6 +94,41 @@ class Todos { ...@@ -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) { updateBadges(data) {
$(document).trigger('todo:toggle', data.count); $(document).trigger('todo:toggle', data.count);
document.querySelector('.todos-pending .badge').innerHTML = data.count; document.querySelector('.todos-pending .badge').innerHTML = data.count;
......
...@@ -22,12 +22,12 @@ class Dashboard::TodosController < Dashboard::ApplicationController ...@@ -22,12 +22,12 @@ class Dashboard::TodosController < Dashboard::ApplicationController
end end
def destroy_all 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| respond_to do |format|
format.html { redirect_to dashboard_todos_path, notice: 'All todos were marked as done.' } format.html { redirect_to dashboard_todos_path, notice: 'All todos were marked as done.' }
format.js { head :ok } format.js { head :ok }
format.json { render json: todos_counts } format.json { render json: todos_counts.merge(updated_ids: updated_ids) }
end end
end end
...@@ -37,6 +37,12 @@ class Dashboard::TodosController < Dashboard::ApplicationController ...@@ -37,6 +37,12 @@ class Dashboard::TodosController < Dashboard::ApplicationController
render json: todos_counts render json: todos_counts
end 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 # Used in TodosHelper also
def self.todos_count_format(count) def self.todos_count_format(count)
count >= 100 ? '99+' : count count >= 100 ? '99+' : count
......
...@@ -201,10 +201,12 @@ class TodoService ...@@ -201,10 +201,12 @@ class TodoService
def update_todos_state_by_ids(ids, current_user, state) def update_todos_state_by_ids(ids, current_user, state)
todos = current_user.todos.where(id: ids) todos = current_user.todos.where(id: ids)
# Only return those that are not really on that state # Only update those that are not really on that state
marked_todos = todos.where.not(state: state).update_all(state: state) todos = todos.where.not(state: state)
todos_ids = todos.pluck(:id)
todos.update_all(state: state)
current_user.update_todos_count_cache current_user.update_todos_count_cache
marked_todos todos_ids
end end
def create_todos(users, attributes) def create_todos(users, attributes)
......
...@@ -36,14 +36,14 @@ ...@@ -36,14 +36,14 @@
- if todo.pending? - if todo.pending?
.todo-actions .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 Done
= icon('spinner spin') = 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 Undo
= icon('spinner spin') = icon('spinner spin')
- else - else
.todo-actions .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 Add todo
= icon('spinner spin') = icon('spinner spin')
...@@ -19,9 +19,12 @@ ...@@ -19,9 +19,12 @@
.nav-controls .nav-controls
- if @todos.any?(&:pending?) - 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 Mark all as done
= icon('spinner spin') = 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 .todos-filters
.row-content-block.second-block .row-content-block.second-block
...@@ -67,12 +70,16 @@ ...@@ -67,12 +70,16 @@
.js-todos-all .js-todos-all
- if @todos.any? - if @todos.any?
.js-todos-options{ data: {per_page: @todos.limit_value, current_page: @todos.current_page, total_pages: @todos.total_pages} } .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 .panel.panel-default.panel-small.panel-without-border
%ul.content-list.todos-list %ul.content-list.todos-list
= render @todos = render @todos
= paginate @todos, theme: "gitlab" = 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? - elsif current_user.todos.any?
.todos-all-done .todos-all-done
= render "shared/empty_states/icons/todos_all_done.svg" = render "shared/empty_states/icons/todos_all_done.svg"
......
---
title: Add Undo mark all as done to Todos
merge_request: 9890
author: Jacopo Beschi @jacopo-beschi
...@@ -13,6 +13,7 @@ resource :dashboard, controller: 'dashboard', only: [] do ...@@ -13,6 +13,7 @@ resource :dashboard, controller: 'dashboard', only: [] do
resources :todos, only: [:index, :destroy] do resources :todos, only: [:index, :destroy] do
collection do collection do
delete :destroy_all delete :destroy_all
patch :bulk_restore
end end
member do member do
patch :restore patch :restore
......
...@@ -159,7 +159,11 @@ class Spinach::Features::DashboardTodos < Spinach::FeatureSteps ...@@ -159,7 +159,11 @@ class Spinach::Features::DashboardTodos < Spinach::FeatureSteps
end end
def should_not_see_todo(title) 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 end
def john_doe def john_doe
......
...@@ -22,7 +22,7 @@ module API ...@@ -22,7 +22,7 @@ module API
status(200) status(200)
todos = TodosFinder.new(current_user, params).execute 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 end
end end
......
...@@ -49,4 +49,18 @@ describe Dashboard::TodosController do ...@@ -49,4 +49,18 @@ describe Dashboard::TodosController do
expect(json_response).to eq({ "count" => "1", "done_count" => "0" }) expect(json_response).to eq({ "count" => "1", "done_count" => "0" })
end end
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 end
...@@ -31,7 +31,7 @@ describe 'Dashboard Todos', feature: true do ...@@ -31,7 +31,7 @@ describe 'Dashboard Todos', feature: true do
end end
it 'shows due date as today' do it 'shows due date as today' do
page.within first('.todo') do within first('.todo') do
expect(page).to have_content 'Due today' expect(page).to have_content 'Due today'
end end
end end
...@@ -184,6 +184,60 @@ describe 'Dashboard Todos', feature: true do ...@@ -184,6 +184,60 @@ describe 'Dashboard Todos', feature: true do
expect(page).to have_content "You're all done!" expect(page).to have_content "You're all done!"
expect(page).not_to have_selector('.gl-pagination') expect(page).not_to have_selector('.gl-pagination')
end 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
end end
......
...@@ -298,6 +298,10 @@ describe TodoService, services: true do ...@@ -298,6 +298,10 @@ describe TodoService, services: true do
expect(second_todo.reload.state?(new_state)).to be true expect(second_todo.reload.state?(new_state)).to be true
end 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 describe 'cached counts' do
it 'updates when todos change' do it 'updates when todos change' do
expect(john_doe.todos.where(state: new_state).count).to eq(0) expect(john_doe.todos.where(state: new_state).count).to eq(0)
...@@ -779,29 +783,27 @@ describe TodoService, services: true do ...@@ -779,29 +783,27 @@ describe TodoService, services: true do
.to change { todo.reload.state }.from('pending').to('done') .to change { todo.reload.state }.from('pending').to('done')
end 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) 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 end
context 'when some of the todos are done already' do context 'when some of the todos are done already' do
before do let!(:first_todo) { create(:todo, :mentioned, user: john_doe, target: issue, project: project) }
create(:todo, :mentioned, user: john_doe, target: issue, project: project) let!(:second_todo) { create(:todo, :mentioned, user: john_doe, target: another_issue, project: project) }
create(:todo, :mentioned, user: john_doe, target: another_issue, project: project)
end
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) 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 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(issue, john_doe)
TodoService.new.mark_pending_todos_as_done(another_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
end end
......
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