Add 'Undo' to Todos in the Done tab
Added the ability to 'Undo' the todos marked complete in the 'Done' tab of the Todo dashboard. Added the ability to 'Undo mark all as done' todos marked complete with 'Mark all as done' in the 'Todo' tab of the Todo dashboard.
This commit is contained in:
parent
69c1a9ae86
commit
e20ffc6e00
|
@ -1,146 +1,146 @@
|
||||||
/* eslint-disable class-methods-use-this, no-new, func-names, no-unneeded-ternary, object-shorthand, quote-props, no-param-reassign, max-len */
|
/* eslint-disable class-methods-use-this, no-unneeded-ternary, quote-props */
|
||||||
/* global UsersSelect */
|
/* global UsersSelect */
|
||||||
|
|
||||||
((global) => {
|
class Todos {
|
||||||
class Todos {
|
constructor() {
|
||||||
constructor() {
|
this.initFilters();
|
||||||
this.initFilters();
|
this.bindEvents();
|
||||||
this.bindEvents();
|
|
||||||
|
|
||||||
this.cleanupWrapper = this.cleanup.bind(this);
|
this.cleanupWrapper = this.cleanup.bind(this);
|
||||||
document.addEventListener('beforeunload', this.cleanupWrapper);
|
document.addEventListener('beforeunload', this.cleanupWrapper);
|
||||||
}
|
}
|
||||||
|
|
||||||
cleanup() {
|
cleanup() {
|
||||||
this.unbindEvents();
|
this.unbindEvents();
|
||||||
document.removeEventListener('beforeunload', this.cleanupWrapper);
|
document.removeEventListener('beforeunload', this.cleanupWrapper);
|
||||||
}
|
}
|
||||||
|
|
||||||
unbindEvents() {
|
unbindEvents() {
|
||||||
$('.js-done-todo, .js-undo-todo').off('click', this.updateStateClickedWrapper);
|
$('.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').off('click', this.allDoneClickedWrapper);
|
||||||
$('.todo').off('click', this.goToTodoUrl);
|
$('.todo').off('click', this.goToTodoUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
bindEvents() {
|
bindEvents() {
|
||||||
this.updateStateClickedWrapper = this.updateStateClicked.bind(this);
|
this.updateRowStateClickedWrapper = this.updateRowStateClicked.bind(this);
|
||||||
this.allDoneClickedWrapper = this.allDoneClicked.bind(this);
|
this.allDoneClickedWrapper = this.allDoneClicked.bind(this);
|
||||||
|
|
||||||
$('.js-done-todo, .js-undo-todo').on('click', this.updateStateClickedWrapper);
|
$('.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').on('click', this.allDoneClickedWrapper);
|
||||||
$('.todo').on('click', this.goToTodoUrl);
|
$('.todo').on('click', this.goToTodoUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
initFilters() {
|
initFilters() {
|
||||||
new UsersSelect();
|
this.initFilterDropdown($('.js-project-search'), 'project_id', ['text']);
|
||||||
this.initFilterDropdown($('.js-project-search'), 'project_id', ['text']);
|
this.initFilterDropdown($('.js-type-search'), 'type');
|
||||||
this.initFilterDropdown($('.js-type-search'), 'type');
|
this.initFilterDropdown($('.js-action-search'), 'action_id');
|
||||||
this.initFilterDropdown($('.js-action-search'), 'action_id');
|
|
||||||
|
|
||||||
$('form.filter-form').on('submit', function (event) {
|
$('form.filter-form').on('submit', function applyFilters(event) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
gl.utils.visitUrl(`${this.action}&${$(this).serialize()}`);
|
gl.utils.visitUrl(`${this.action}&${$(this).serialize()}`);
|
||||||
});
|
});
|
||||||
}
|
return new UsersSelect();
|
||||||
|
}
|
||||||
|
|
||||||
initFilterDropdown($dropdown, fieldName, searchFields) {
|
initFilterDropdown($dropdown, fieldName, searchFields) {
|
||||||
$dropdown.glDropdown({
|
$dropdown.glDropdown({
|
||||||
fieldName,
|
fieldName,
|
||||||
selectable: true,
|
selectable: true,
|
||||||
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: () => $dropdown.closest('form.filter-form').submit(),
|
||||||
return $dropdown.closest('form.filter-form').submit();
|
});
|
||||||
},
|
}
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
updateStateClicked(e) {
|
updateRowStateClicked(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const target = e.target;
|
|
||||||
target.setAttribute('disabled', '');
|
|
||||||
target.classList.add('disabled');
|
|
||||||
$.ajax({
|
|
||||||
type: 'POST',
|
|
||||||
url: target.getAttribute('href'),
|
|
||||||
dataType: 'json',
|
|
||||||
data: {
|
|
||||||
'_method': target.getAttribute('data-method'),
|
|
||||||
},
|
|
||||||
success: (data) => {
|
|
||||||
this.updateState(target);
|
|
||||||
this.updateBadges(data);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
allDoneClicked(e) {
|
const target = e.target;
|
||||||
e.preventDefault();
|
target.setAttribute('disabled', '');
|
||||||
const $target = $(e.currentTarget);
|
target.classList.add('disabled');
|
||||||
$target.disable();
|
$.ajax({
|
||||||
$.ajax({
|
type: 'POST',
|
||||||
type: 'POST',
|
url: target.getAttribute('href'),
|
||||||
url: $target.attr('href'),
|
dataType: 'json',
|
||||||
dataType: 'json',
|
data: {
|
||||||
data: {
|
'_method': target.getAttribute('data-method'),
|
||||||
'_method': 'delete',
|
},
|
||||||
},
|
success: (data) => {
|
||||||
success: (data) => {
|
this.updateRowState(target);
|
||||||
$target.remove();
|
return this.updateBadges(data);
|
||||||
$('.js-todos-all').html('<div class="nothing-here-block">You\'re all done!</div>');
|
},
|
||||||
this.updateBadges(data);
|
});
|
||||||
},
|
}
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
updateState(target) {
|
allDoneClicked(e) {
|
||||||
const row = target.closest('li');
|
e.preventDefault();
|
||||||
const restoreBtn = row.querySelector('.js-undo-todo');
|
const $target = $(e.currentTarget);
|
||||||
const doneBtn = row.querySelector('.js-done-todo');
|
$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);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
target.removeAttribute('disabled');
|
updateRowState(target) {
|
||||||
target.classList.remove('disabled');
|
const row = target.closest('li');
|
||||||
target.classList.add('hidden');
|
const restoreBtn = row.querySelector('.js-undo-todo');
|
||||||
|
const doneBtn = row.querySelector('.js-done-todo');
|
||||||
|
|
||||||
if (target === doneBtn) {
|
target.classList.add('hidden');
|
||||||
row.classList.add('done-reversible');
|
target.removeAttribute('disabled');
|
||||||
restoreBtn.classList.remove('hidden');
|
target.classList.remove('disabled');
|
||||||
} else {
|
|
||||||
row.classList.remove('done-reversible');
|
|
||||||
doneBtn.classList.remove('hidden');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
updateBadges(data) {
|
if (target === doneBtn) {
|
||||||
$(document).trigger('todo:toggle', data.count);
|
row.classList.add('done-reversible');
|
||||||
$('.todos-pending .badge').text(data.count);
|
restoreBtn.classList.remove('hidden');
|
||||||
$('.todos-done .badge').text(data.done_count);
|
} else if (target === restoreBtn) {
|
||||||
}
|
row.classList.remove('done-reversible');
|
||||||
|
doneBtn.classList.remove('hidden');
|
||||||
goToTodoUrl(e) {
|
} else {
|
||||||
const todoLink = this.dataset.url;
|
row.parentNode.removeChild(row);
|
||||||
|
|
||||||
if (!todoLink) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (gl.utils.isMetaClick(e)) {
|
|
||||||
const windowTarget = '_blank';
|
|
||||||
const selected = e.target;
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
if (selected.tagName === 'IMG') {
|
|
||||||
const avatarUrl = selected.parentElement.getAttribute('href');
|
|
||||||
window.open(avatarUrl, windowTarget);
|
|
||||||
} else {
|
|
||||||
window.open(todoLink, windowTarget);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
gl.utils.visitUrl(todoLink);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
global.Todos = Todos;
|
updateBadges(data) {
|
||||||
})(window.gl || (window.gl = {}));
|
$(document).trigger('todo:toggle', data.count);
|
||||||
|
document.querySelector('.todos-pending .badge').innerHTML = data.count;
|
||||||
|
document.querySelector('.todos-done .badge').innerHTML = data.done_count;
|
||||||
|
}
|
||||||
|
|
||||||
|
goToTodoUrl(e) {
|
||||||
|
const todoLink = this.dataset.url;
|
||||||
|
|
||||||
|
if (!todoLink) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (gl.utils.isMetaClick(e)) {
|
||||||
|
const windowTarget = '_blank';
|
||||||
|
const selected = e.target;
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
if (selected.tagName === 'IMG') {
|
||||||
|
const avatarUrl = selected.parentElement.getAttribute('href');
|
||||||
|
window.open(avatarUrl, windowTarget);
|
||||||
|
} else {
|
||||||
|
window.open(todoLink, windowTarget);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
gl.utils.visitUrl(todoLink);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.gl = window.gl || {};
|
||||||
|
gl.Todos = Todos;
|
||||||
|
|
|
@ -42,3 +42,8 @@
|
||||||
= 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' do
|
||||||
Undo
|
Undo
|
||||||
= icon('spinner spin')
|
= icon('spinner spin')
|
||||||
|
- else
|
||||||
|
.todo-actions
|
||||||
|
= link_to restore_dashboard_todo_path(todo), method: :patch, class: 'btn btn-loading js-add-todo' do
|
||||||
|
Add todo
|
||||||
|
= icon('spinner spin')
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
---
|
||||||
|
title: Add Undo to Todos in the Done tab
|
||||||
|
merge_request: 8782
|
||||||
|
author: Jacopo Beschi @jacopo-beschi
|
|
@ -38,7 +38,9 @@ describe 'Dashboard Todos', feature: true do
|
||||||
|
|
||||||
shared_examples 'deleting the todo' do
|
shared_examples 'deleting the todo' do
|
||||||
before do
|
before do
|
||||||
first('.js-done-todo').click
|
within first('.todo') do
|
||||||
|
click_link 'Done'
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'is marked as done-reversible in the list' do
|
it 'is marked as done-reversible in the list' do
|
||||||
|
@ -62,9 +64,11 @@ describe 'Dashboard Todos', feature: true do
|
||||||
|
|
||||||
shared_examples 'deleting and restoring the todo' do
|
shared_examples 'deleting and restoring the todo' do
|
||||||
before do
|
before do
|
||||||
first('.js-done-todo').click
|
within first('.todo') do
|
||||||
wait_for_ajax
|
click_link 'Done'
|
||||||
first('.js-undo-todo').click
|
wait_for_ajax
|
||||||
|
click_link 'Undo'
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'is marked back as pending in the list' do
|
it 'is marked back as pending in the list' do
|
||||||
|
@ -97,6 +101,35 @@ describe 'Dashboard Todos', feature: true do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'User has done todos', js: true do
|
||||||
|
before do
|
||||||
|
create(:todo, :mentioned, :done, user: user, project: project, target: issue, author: author)
|
||||||
|
login_as(user)
|
||||||
|
visit dashboard_todos_path(state: :done)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'has the done todo present' do
|
||||||
|
expect(page).to have_selector('.todos-list .todo.todo-done', count: 1)
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'restoring the todo' do
|
||||||
|
before do
|
||||||
|
within first('.todo') do
|
||||||
|
click_link 'Add todo'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'is removed from the list' do
|
||||||
|
expect(page).not_to have_selector('.todos-list .todo.todo-done')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'updates todo count' do
|
||||||
|
expect(page).to have_content 'To do 1'
|
||||||
|
expect(page).to have_content 'Done 0'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
context 'User has Todos with labels spanning multiple projects' do
|
context 'User has Todos with labels spanning multiple projects' do
|
||||||
before do
|
before do
|
||||||
label1 = create(:label, project: project)
|
label1 = create(:label, project: project)
|
||||||
|
@ -143,7 +176,7 @@ describe 'Dashboard Todos', feature: true do
|
||||||
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
|
||||||
click_link('Mark all as done')
|
click_link 'Mark all as done'
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'shows "All done" message!' do
|
it 'shows "All done" message!' do
|
||||||
|
|
Loading…
Reference in New Issue