Merge branch '25465-todo-done-clicking-is-kind-of-unsafe' into 'master'
Todo done clicking is kind of unusable. Closes #25465 See merge request !8691
This commit is contained in:
commit
6eb45df2fd
|
|
@ -1,28 +1,34 @@
|
||||||
/* eslint-disable class-methods-use-this, no-new, func-names, prefer-template, no-unneeded-ternary, object-shorthand, space-before-function-paren, comma-dangle, quote-props, consistent-return, no-else-return, no-param-reassign, max-len */
|
/* eslint-disable class-methods-use-this, no-new, func-names, no-unneeded-ternary, object-shorthand, quote-props, no-param-reassign, max-len */
|
||||||
/* global UsersSelect */
|
/* global UsersSelect */
|
||||||
|
|
||||||
((global) => {
|
((global) => {
|
||||||
class Todos {
|
class Todos {
|
||||||
constructor({ el } = {}) {
|
constructor() {
|
||||||
this.allDoneClicked = this.allDoneClicked.bind(this);
|
|
||||||
this.doneClicked = this.doneClicked.bind(this);
|
|
||||||
this.el = el || $('.js-todos-options');
|
|
||||||
this.perPage = this.el.data('perPage');
|
|
||||||
this.clearListeners();
|
|
||||||
this.initBtnListeners();
|
|
||||||
this.initFilters();
|
this.initFilters();
|
||||||
|
this.bindEvents();
|
||||||
|
|
||||||
|
this.cleanupWrapper = this.cleanup.bind(this);
|
||||||
|
document.addEventListener('beforeunload', this.cleanupWrapper);
|
||||||
}
|
}
|
||||||
|
|
||||||
clearListeners() {
|
cleanup() {
|
||||||
$('.done-todo').off('click');
|
this.unbindEvents();
|
||||||
$('.js-todos-mark-all').off('click');
|
document.removeEventListener('beforeunload', this.cleanupWrapper);
|
||||||
return $('.todo').off('click');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
initBtnListeners() {
|
unbindEvents() {
|
||||||
$('.done-todo').on('click', this.doneClicked);
|
$('.js-done-todo, .js-undo-todo').off('click', this.updateStateClickedWrapper);
|
||||||
$('.js-todos-mark-all').on('click', this.allDoneClicked);
|
$('.js-todos-mark-all').off('click', this.allDoneClickedWrapper);
|
||||||
return $('.todo').on('click', this.goToTodoUrl);
|
$('.todo').off('click', this.goToTodoUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
bindEvents() {
|
||||||
|
this.updateStateClickedWrapper = this.updateStateClicked.bind(this);
|
||||||
|
this.allDoneClickedWrapper = this.allDoneClicked.bind(this);
|
||||||
|
|
||||||
|
$('.js-done-todo, .js-undo-todo').on('click', this.updateStateClickedWrapper);
|
||||||
|
$('.js-todos-mark-all').on('click', this.allDoneClickedWrapper);
|
||||||
|
$('.todo').on('click', this.goToTodoUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
initFilters() {
|
initFilters() {
|
||||||
|
|
@ -33,7 +39,7 @@
|
||||||
|
|
||||||
$('form.filter-form').on('submit', function (event) {
|
$('form.filter-form').on('submit', function (event) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
gl.utils.visitUrl(this.action + '&' + $(this).serialize());
|
gl.utils.visitUrl(`${this.action}&${$(this).serialize()}`);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -44,105 +50,72 @@
|
||||||
filterable: searchFields ? true : false,
|
filterable: searchFields ? true : false,
|
||||||
search: { fields: searchFields },
|
search: { fields: searchFields },
|
||||||
data: $dropdown.data('data'),
|
data: $dropdown.data('data'),
|
||||||
clicked: function() {
|
clicked: function () {
|
||||||
return $dropdown.closest('form.filter-form').submit();
|
return $dropdown.closest('form.filter-form').submit();
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
doneClicked(e) {
|
updateStateClicked(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopImmediatePropagation();
|
const target = e.target;
|
||||||
const $target = $(e.currentTarget);
|
target.setAttribute('disabled', '');
|
||||||
$target.disable();
|
target.classList.add('disabled');
|
||||||
return $.ajax({
|
$.ajax({
|
||||||
type: 'POST',
|
type: 'POST',
|
||||||
url: $target.attr('href'),
|
url: target.getAttribute('href'),
|
||||||
dataType: 'json',
|
dataType: 'json',
|
||||||
data: {
|
data: {
|
||||||
'_method': 'delete'
|
'_method': target.getAttribute('data-method'),
|
||||||
},
|
},
|
||||||
success: (data) => {
|
success: (data) => {
|
||||||
this.redirectIfNeeded(data.count);
|
this.updateState(target);
|
||||||
this.clearDone($target.closest('li'));
|
this.updateBadges(data);
|
||||||
return this.updateBadges(data);
|
},
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
allDoneClicked(e) {
|
allDoneClicked(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopImmediatePropagation();
|
|
||||||
const $target = $(e.currentTarget);
|
const $target = $(e.currentTarget);
|
||||||
$target.disable();
|
$target.disable();
|
||||||
return $.ajax({
|
$.ajax({
|
||||||
type: 'POST',
|
type: 'POST',
|
||||||
url: $target.attr('href'),
|
url: $target.attr('href'),
|
||||||
dataType: 'json',
|
dataType: 'json',
|
||||||
data: {
|
data: {
|
||||||
'_method': 'delete'
|
'_method': 'delete',
|
||||||
},
|
},
|
||||||
success: (data) => {
|
success: (data) => {
|
||||||
$target.remove();
|
$target.remove();
|
||||||
$('.js-todos-all').html('<div class="nothing-here-block">You\'re all done!</div>');
|
$('.js-todos-all').html('<div class="nothing-here-block">You\'re all done!</div>');
|
||||||
return this.updateBadges(data);
|
this.updateBadges(data);
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
clearDone($row) {
|
updateState(target) {
|
||||||
const $ul = $row.closest('ul');
|
const row = target.closest('li');
|
||||||
$row.remove();
|
const restoreBtn = row.querySelector('.js-undo-todo');
|
||||||
if (!$ul.find('li').length) {
|
const doneBtn = row.querySelector('.js-done-todo');
|
||||||
return $ul.parents('.panel').remove();
|
|
||||||
|
target.removeAttribute('disabled');
|
||||||
|
target.classList.remove('disabled');
|
||||||
|
target.classList.add('hidden');
|
||||||
|
|
||||||
|
if (target === doneBtn) {
|
||||||
|
row.classList.add('done-reversible');
|
||||||
|
restoreBtn.classList.remove('hidden');
|
||||||
|
} else {
|
||||||
|
row.classList.remove('done-reversible');
|
||||||
|
doneBtn.classList.remove('hidden');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
updateBadges(data) {
|
updateBadges(data) {
|
||||||
$(document).trigger('todo:toggle', data.count);
|
$(document).trigger('todo:toggle', data.count);
|
||||||
$('.todos-pending .badge').text(data.count);
|
$('.todos-pending .badge').text(data.count);
|
||||||
return $('.todos-done .badge').text(data.done_count);
|
$('.todos-done .badge').text(data.done_count);
|
||||||
}
|
|
||||||
|
|
||||||
getTotalPages() {
|
|
||||||
return this.el.data('totalPages');
|
|
||||||
}
|
|
||||||
|
|
||||||
getCurrentPage() {
|
|
||||||
return this.el.data('currentPage');
|
|
||||||
}
|
|
||||||
|
|
||||||
getTodosPerPage() {
|
|
||||||
return this.el.data('perPage');
|
|
||||||
}
|
|
||||||
|
|
||||||
redirectIfNeeded(total) {
|
|
||||||
const currPages = this.getTotalPages();
|
|
||||||
const currPage = this.getCurrentPage();
|
|
||||||
|
|
||||||
// Refresh if no remaining Todos
|
|
||||||
if (!total) {
|
|
||||||
window.location.reload();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Do nothing if no pagination
|
|
||||||
if (!currPages) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const newPages = Math.ceil(total / this.getTodosPerPage());
|
|
||||||
let url = location.href;
|
|
||||||
|
|
||||||
if (newPages !== currPages) {
|
|
||||||
// Redirect to previous page if there's one available
|
|
||||||
if (currPages > 1 && currPage === currPages) {
|
|
||||||
const pageParams = {
|
|
||||||
page: currPages - 1
|
|
||||||
};
|
|
||||||
url = gl.utils.mergeUrlParams(pageParams, url);
|
|
||||||
}
|
|
||||||
return gl.utils.visitUrl(url);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
goToTodoUrl(e) {
|
goToTodoUrl(e) {
|
||||||
|
|
@ -159,12 +132,12 @@
|
||||||
|
|
||||||
if (selected.tagName === 'IMG') {
|
if (selected.tagName === 'IMG') {
|
||||||
const avatarUrl = selected.parentElement.getAttribute('href');
|
const avatarUrl = selected.parentElement.getAttribute('href');
|
||||||
return window.open(avatarUrl, windowTarget);
|
window.open(avatarUrl, windowTarget);
|
||||||
} else {
|
} else {
|
||||||
return window.open(todoLink, windowTarget);
|
window.open(todoLink, windowTarget);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return gl.utils.visitUrl(todoLink);
|
gl.utils.visitUrl(todoLink);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -60,6 +60,18 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.todos-list > .todo.todo-pending.done-reversible {
|
||||||
|
background-color: $gray-light;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: $border-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.todo-item {
|
.todo-item {
|
||||||
.todo-title {
|
.todo-title {
|
||||||
@include str-truncated(calc(100% - 174px));
|
@include str-truncated(calc(100% - 174px));
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,12 @@ class Dashboard::TodosController < Dashboard::ApplicationController
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def restore
|
||||||
|
TodoService.new.mark_todos_as_pending_by_ids([params[:id]], current_user)
|
||||||
|
|
||||||
|
render json: todos_counts
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def find_todos
|
def find_todos
|
||||||
|
|
|
||||||
|
|
@ -170,16 +170,20 @@ class TodoService
|
||||||
|
|
||||||
# When user marks some todos as done
|
# When user marks some todos as done
|
||||||
def mark_todos_as_done(todos, current_user)
|
def mark_todos_as_done(todos, current_user)
|
||||||
mark_todos_as_done_by_ids(todos.select(&:id), current_user)
|
update_todos_state_by_ids(todos.select(&:id), current_user, :done)
|
||||||
end
|
end
|
||||||
|
|
||||||
def mark_todos_as_done_by_ids(ids, current_user)
|
def mark_todos_as_done_by_ids(ids, current_user)
|
||||||
todos = current_user.todos.where(id: ids)
|
update_todos_state_by_ids(ids, current_user, :done)
|
||||||
|
end
|
||||||
|
|
||||||
# Only return those that are not really on that state
|
# When user marks some todos as pending
|
||||||
marked_todos = todos.where.not(state: :done).update_all(state: :done)
|
def mark_todos_as_pending(todos, current_user)
|
||||||
current_user.update_todos_count_cache
|
update_todos_state_by_ids(todos.select(&:id), current_user, :pending)
|
||||||
marked_todos
|
end
|
||||||
|
|
||||||
|
def mark_todos_as_pending_by_ids(ids, current_user)
|
||||||
|
update_todos_state_by_ids(ids, current_user, :pending)
|
||||||
end
|
end
|
||||||
|
|
||||||
# When user marks an issue as todo
|
# When user marks an issue as todo
|
||||||
|
|
@ -194,6 +198,15 @@ class TodoService
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
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)
|
||||||
|
current_user.update_todos_count_cache
|
||||||
|
marked_todos
|
||||||
|
end
|
||||||
|
|
||||||
def create_todos(users, attributes)
|
def create_todos(users, attributes)
|
||||||
Array(users).map do |user|
|
Array(users).map do |user|
|
||||||
next if pending_todos(user, attributes).exists?
|
next if pending_todos(user, attributes).exists?
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,9 @@
|
||||||
|
|
||||||
- if todo.pending?
|
- if todo.pending?
|
||||||
.todo-actions
|
.todo-actions
|
||||||
= link_to [:dashboard, todo], method: :delete, class: 'btn btn-loading done-todo' do
|
= link_to [:dashboard, todo], method: :delete, class: 'btn btn-loading js-done-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
|
||||||
|
Undo
|
||||||
|
= icon('spinner spin')
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
---
|
||||||
|
title: Todo done clicking is kind of unusable
|
||||||
|
merge_request: 8691
|
||||||
|
author: Jacopo Beschi @jacopo-beschi
|
||||||
|
|
@ -14,6 +14,9 @@ resource :dashboard, controller: 'dashboard', only: [] do
|
||||||
collection do
|
collection do
|
||||||
delete :destroy_all
|
delete :destroy_all
|
||||||
end
|
end
|
||||||
|
member do
|
||||||
|
patch :restore
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
resources :projects, only: [:index] do
|
resources :projects, only: [:index] do
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,7 @@ class Spinach::Features::DashboardTodos < Spinach::FeatureSteps
|
||||||
page.within('.todos-pending-count') { expect(page).to have_content '3' }
|
page.within('.todos-pending-count') { expect(page).to have_content '3' }
|
||||||
expect(page).to have_content 'To do 3'
|
expect(page).to have_content 'To do 3'
|
||||||
expect(page).to have_content 'Done 1'
|
expect(page).to have_content 'Done 1'
|
||||||
should_not_see_todo "John Doe assigned you merge request #{merge_request.to_reference(full: true)}"
|
should_see_todo(1, "John Doe assigned you merge request #{merge_request.to_reference(full: true)}", merge_request.title, state: :done_reversible)
|
||||||
end
|
end
|
||||||
|
|
||||||
step 'I mark all todos as done' do
|
step 'I mark all todos as done' do
|
||||||
|
|
@ -71,7 +71,7 @@ class Spinach::Features::DashboardTodos < Spinach::FeatureSteps
|
||||||
click_link 'Done 1'
|
click_link 'Done 1'
|
||||||
|
|
||||||
expect(page).to have_link project.name_with_namespace
|
expect(page).to have_link project.name_with_namespace
|
||||||
should_see_todo(1, "John Doe assigned you merge request #{merge_request.to_reference(full: true)}", merge_request.title, false)
|
should_see_todo(1, "John Doe assigned you merge request #{merge_request.to_reference(full: true)}", merge_request.title, state: :done_irreversible)
|
||||||
end
|
end
|
||||||
|
|
||||||
step 'I should see all todos marked as done' do
|
step 'I should see all todos marked as done' do
|
||||||
|
|
@ -81,10 +81,10 @@ class Spinach::Features::DashboardTodos < Spinach::FeatureSteps
|
||||||
click_link 'Done 4'
|
click_link 'Done 4'
|
||||||
|
|
||||||
expect(page).to have_link project.name_with_namespace
|
expect(page).to have_link project.name_with_namespace
|
||||||
should_see_todo(1, "John Doe assigned you merge request #{merge_request_reference}", merge_request.title, false)
|
should_see_todo(1, "John Doe assigned you merge request #{merge_request_reference}", merge_request.title, state: :done_irreversible)
|
||||||
should_see_todo(2, "John Doe mentioned you on issue #{issue_reference}", "#{current_user.to_reference} Wdyt?", false)
|
should_see_todo(2, "John Doe mentioned you on issue #{issue_reference}", "#{current_user.to_reference} Wdyt?", state: :done_irreversible)
|
||||||
should_see_todo(3, "John Doe assigned you issue #{issue_reference}", issue.title, false)
|
should_see_todo(3, "John Doe assigned you issue #{issue_reference}", issue.title, state: :done_irreversible)
|
||||||
should_see_todo(4, "Mary Jane mentioned you on issue #{issue_reference}", issue.title, false)
|
should_see_todo(4, "Mary Jane mentioned you on issue #{issue_reference}", issue.title, state: :done_irreversible)
|
||||||
end
|
end
|
||||||
|
|
||||||
step 'I filter by "Enterprise"' do
|
step 'I filter by "Enterprise"' do
|
||||||
|
|
@ -140,15 +140,20 @@ class Spinach::Features::DashboardTodos < Spinach::FeatureSteps
|
||||||
page.should have_css('.identifier', text: 'Merge Request !1')
|
page.should have_css('.identifier', text: 'Merge Request !1')
|
||||||
end
|
end
|
||||||
|
|
||||||
def should_see_todo(position, title, body, pending = true)
|
def should_see_todo(position, title, body, state: :pending)
|
||||||
page.within(".todo:nth-child(#{position})") do
|
page.within(".todo:nth-child(#{position})") do
|
||||||
expect(page).to have_content title
|
expect(page).to have_content title
|
||||||
expect(page).to have_content body
|
expect(page).to have_content body
|
||||||
|
|
||||||
if pending
|
if state == :pending
|
||||||
expect(page).to have_link 'Done'
|
expect(page).to have_link 'Done'
|
||||||
else
|
elsif state == :done_reversible
|
||||||
|
expect(page).to have_link 'Undo'
|
||||||
|
elsif state == :done_irreversible
|
||||||
|
expect(page).not_to have_link 'Undo'
|
||||||
expect(page).not_to have_link 'Done'
|
expect(page).not_to have_link 'Done'
|
||||||
|
else
|
||||||
|
raise 'Invalid state given, valid states: :pending, :done_reversible, :done_irreversible'
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,19 @@
|
||||||
require 'spec_helper'
|
require 'spec_helper'
|
||||||
|
|
||||||
describe Dashboard::TodosController do
|
describe Dashboard::TodosController do
|
||||||
|
include ApiHelpers
|
||||||
|
|
||||||
let(:user) { create(:user) }
|
let(:user) { create(:user) }
|
||||||
|
let(:author) { create(:user) }
|
||||||
let(:project) { create(:empty_project) }
|
let(:project) { create(:empty_project) }
|
||||||
let(:todo_service) { TodoService.new }
|
let(:todo_service) { TodoService.new }
|
||||||
|
|
||||||
describe 'GET #index' do
|
before do
|
||||||
before do
|
sign_in(user)
|
||||||
sign_in(user)
|
project.team << [user, :developer]
|
||||||
project.team << [user, :developer]
|
end
|
||||||
end
|
|
||||||
|
|
||||||
|
describe 'GET #index' do
|
||||||
context 'when using pagination' do
|
context 'when using pagination' do
|
||||||
let(:last_page) { user.todos.page.total_pages }
|
let(:last_page) { user.todos.page.total_pages }
|
||||||
let!(:issues) { create_list(:issue, 2, project: project, assignee: user) }
|
let!(:issues) { create_list(:issue, 2, project: project, assignee: user) }
|
||||||
|
|
@ -34,4 +37,16 @@ describe Dashboard::TodosController do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe 'PATCH #restore' do
|
||||||
|
let(:todo) { create(:todo, :done, user: user, project: project, author: author) }
|
||||||
|
|
||||||
|
it 'restores the todo to pending state' do
|
||||||
|
patch :restore, id: todo.id
|
||||||
|
|
||||||
|
expect(todo.reload).to be_pending
|
||||||
|
expect(response).to have_http_status(200)
|
||||||
|
expect(json_response).to eq({ "count" => 1, "done_count" => 0 })
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,10 @@ FactoryGirl.define do
|
||||||
action { Todo::UNMERGEABLE }
|
action { Todo::UNMERGEABLE }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
trait :pending do
|
||||||
|
state :pending
|
||||||
|
end
|
||||||
|
|
||||||
trait :done do
|
trait :done do
|
||||||
state :done
|
state :done
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
require 'spec_helper'
|
require 'spec_helper'
|
||||||
|
|
||||||
describe 'Dashboard Todos', feature: true do
|
describe 'Dashboard Todos', feature: true do
|
||||||
|
include WaitForAjax
|
||||||
|
|
||||||
let(:user) { create(:user) }
|
let(:user) { create(:user) }
|
||||||
let(:author) { create(:user) }
|
let(:author) { create(:user) }
|
||||||
let(:project) { create(:project, visibility_level: Gitlab::VisibilityLevel::PUBLIC) }
|
let(:project) { create(:project, visibility_level: Gitlab::VisibilityLevel::PUBLIC) }
|
||||||
|
|
@ -34,39 +36,64 @@ describe 'Dashboard Todos', feature: true do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'deleting the todo' do
|
shared_examples 'deleting the todo' do
|
||||||
before do
|
before do
|
||||||
first('.done-todo').click
|
first('.js-done-todo').click
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'is removed from the list' do
|
it 'is marked as done-reversible in the list' do
|
||||||
expect(page).not_to have_selector('.todos-list .todo')
|
expect(page).to have_selector('.todos-list .todo.todo-pending.done-reversible')
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'shows "All done" message' do
|
it 'shows Undo button' do
|
||||||
expect(page).to have_selector('.todos-all-done', count: 1)
|
expect(page).to have_selector('.js-undo-todo', visible: true)
|
||||||
|
expect(page).to have_selector('.js-done-todo', visible: false)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'updates todo count' do
|
||||||
|
expect(page).to have_content 'To do 0'
|
||||||
|
expect(page).to have_content 'Done 1'
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'has not "All done" message' do
|
||||||
|
expect(page).not_to have_selector('.todos-all-done')
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
shared_examples 'deleting and restoring the todo' do
|
||||||
|
before do
|
||||||
|
first('.js-done-todo').click
|
||||||
|
wait_for_ajax
|
||||||
|
first('.js-undo-todo').click
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'is marked back as pending in the list' do
|
||||||
|
expect(page).not_to have_selector('.todos-list .todo.todo-pending.done-reversible')
|
||||||
|
expect(page).to have_selector('.todos-list .todo.todo-pending')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'shows Done button' do
|
||||||
|
expect(page).to have_selector('.js-undo-todo', visible: false)
|
||||||
|
expect(page).to have_selector('.js-done-todo', visible: true)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'updates todo count' do
|
||||||
|
expect(page).to have_content 'To do 1'
|
||||||
|
expect(page).to have_content 'Done 0'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it_behaves_like 'deleting the todo'
|
||||||
|
it_behaves_like 'deleting and restoring the todo'
|
||||||
|
|
||||||
context 'todo is stale on the page' do
|
context 'todo is stale on the page' do
|
||||||
before do
|
before do
|
||||||
todos = TodosFinder.new(user, state: :pending).execute
|
todos = TodosFinder.new(user, state: :pending).execute
|
||||||
TodoService.new.mark_todos_as_done(todos, user)
|
TodoService.new.mark_todos_as_done(todos, user)
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'deleting the todo' do
|
it_behaves_like 'deleting the todo'
|
||||||
before do
|
it_behaves_like 'deleting and restoring the todo'
|
||||||
first('.done-todo').click
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'is removed from the list' do
|
|
||||||
expect(page).not_to have_selector('.todos-list .todo')
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'shows "All done" message' do
|
|
||||||
expect(page).to have_selector('.todos-all-done', count: 1)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -113,18 +140,6 @@ describe 'Dashboard Todos', feature: true do
|
||||||
expect(page).to have_selector('.gl-pagination .page', count: 2)
|
expect(page).to have_selector('.gl-pagination .page', count: 2)
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'completing last todo from last page', js: true do
|
|
||||||
it 'redirects to the previous page' do
|
|
||||||
visit dashboard_todos_path(page: 2)
|
|
||||||
expect(page).to have_css("#todo_#{Todo.last.id}")
|
|
||||||
|
|
||||||
click_link('Done')
|
|
||||||
|
|
||||||
expect(current_path).to eq dashboard_todos_path
|
|
||||||
expect(page).to have_css("#todo_#{Todo.first.id}")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe 'mark all as done', js: true do
|
describe 'mark all as done', js: true do
|
||||||
before do
|
before do
|
||||||
visit dashboard_todos_path
|
visit dashboard_todos_path
|
||||||
|
|
|
||||||
|
|
@ -287,39 +287,51 @@ describe TodoService, services: true do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
shared_examples 'marking todos as done' do |meth|
|
shared_examples 'updating todos state' do |meth, state, new_state|
|
||||||
let!(:first_todo) { create(:todo, :assigned, user: john_doe, project: project, target: issue, author: author) }
|
let!(:first_todo) { create(:todo, state, user: john_doe, project: project, target: issue, author: author) }
|
||||||
let!(:second_todo) { create(:todo, :assigned, user: john_doe, project: project, target: issue, author: author) }
|
let!(:second_todo) { create(:todo, state, user: john_doe, project: project, target: issue, author: author) }
|
||||||
|
|
||||||
it 'marks related todos for the user as done' do
|
it 'updates related todos for the user with the new_state' do
|
||||||
service.send(meth, collection, john_doe)
|
service.send(meth, collection, john_doe)
|
||||||
|
|
||||||
expect(first_todo.reload).to be_done
|
expect(first_todo.reload.state?(new_state)).to be true
|
||||||
expect(second_todo.reload).to be_done
|
expect(second_todo.reload.state?(new_state)).to be true
|
||||||
end
|
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_done_count).to eq(0)
|
expect(john_doe.todos.where(state: new_state).count).to eq(0)
|
||||||
expect(john_doe.todos_pending_count).to eq(2)
|
expect(john_doe.todos.where(state: state).count).to eq(2)
|
||||||
expect(john_doe).to receive(:update_todos_count_cache).and_call_original
|
expect(john_doe).to receive(:update_todos_count_cache).and_call_original
|
||||||
|
|
||||||
service.send(meth, collection, john_doe)
|
service.send(meth, collection, john_doe)
|
||||||
|
|
||||||
expect(john_doe.todos_done_count).to eq(2)
|
expect(john_doe.todos.where(state: new_state).count).to eq(2)
|
||||||
expect(john_doe.todos_pending_count).to eq(0)
|
expect(john_doe.todos.where(state: state).count).to eq(0)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe '#mark_todos_as_done' do
|
describe '#mark_todos_as_done' do
|
||||||
it_behaves_like 'marking todos as done', :mark_todos_as_done do
|
it_behaves_like 'updating todos state', :mark_todos_as_done, :pending, :done do
|
||||||
let(:collection) { [first_todo, second_todo] }
|
let(:collection) { [first_todo, second_todo] }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe '#mark_todos_as_done_by_ids' do
|
describe '#mark_todos_as_done_by_ids' do
|
||||||
it_behaves_like 'marking todos as done', :mark_todos_as_done_by_ids do
|
it_behaves_like 'updating todos state', :mark_todos_as_done_by_ids, :pending, :done do
|
||||||
|
let(:collection) { [first_todo, second_todo].map(&:id) }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#mark_todos_as_pending' do
|
||||||
|
it_behaves_like 'updating todos state', :mark_todos_as_pending, :done, :pending do
|
||||||
|
let(:collection) { [first_todo, second_todo] }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#mark_todos_as_pending_by_ids' do
|
||||||
|
it_behaves_like 'updating todos state', :mark_todos_as_pending_by_ids, :done, :pending do
|
||||||
let(:collection) { [first_todo, second_todo].map(&:id) }
|
let(:collection) { [first_todo, second_todo].map(&:id) }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue