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:
commit
774e0e3bfe
|
@ -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 {
|
||||||
|
|
||||||
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 {
|
||||||
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 {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
||||||
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
|
||||||
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
|
||||||
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 @@
|
||||||
|
|
||||||
- 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 @@
|
||||||
|
|
||||||
.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 @@
|
||||||
|
|
||||||
.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
|
||||||
.panel.panel-default.panel-small.panel-without-border
|
.js-todos-options{ data: { per_page: @todos.limit_value, current_page: @todos.current_page, total_pages: @todos.total_pages } }
|
||||||
%ul.content-list.todos-list
|
.panel.panel-default.panel-small.panel-without-border
|
||||||
= render @todos
|
%ul.content-list.todos-list
|
||||||
= paginate @todos, theme: "gitlab"
|
= 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?
|
- 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"
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
---
|
||||||
|
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
|
||||||
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
|
||||||
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
|
||||||
|
|
|
@ -20,9 +20,9 @@ module API
|
||||||
desc 'Mark all todos as done'
|
desc 'Mark all todos as done'
|
||||||
delete do
|
delete do
|
||||||
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
|
||||||
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
|
||||||
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
|
||||||
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
|
||||||
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)
|
||||||
|
@ -706,7 +710,7 @@ describe TodoService, services: true do
|
||||||
should_create_todo(user: admin, author: admin, target: mr_unassigned, action: Todo::UNMERGEABLE)
|
should_create_todo(user: admin, author: admin, target: mr_unassigned, action: Todo::UNMERGEABLE)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe '#mark_todo' do
|
describe '#mark_todo' do
|
||||||
it 'creates a todo from a merge request' do
|
it 'creates a todo from a merge request' do
|
||||||
service.mark_todo(mr_unassigned, author)
|
service.mark_todo(mr_unassigned, author)
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue