From e20ffc6e00e917a19fc4b3ee7b0c6ed75c84f001 Mon Sep 17 00:00:00 2001 From: Jacopo Date: Fri, 10 Mar 2017 13:31:30 +0100 Subject: [PATCH] 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. --- app/assets/javascripts/todos.js | 254 +++++++++--------- app/views/dashboard/todos/_todo.html.haml | 5 + ...7114-add-undo-to-todos-in-the-done-tab.yml | 4 + spec/features/todos/todos_spec.rb | 43 ++- 4 files changed, 174 insertions(+), 132 deletions(-) create mode 100644 changelogs/unreleased/27114-add-undo-to-todos-in-the-done-tab.yml diff --git a/app/assets/javascripts/todos.js b/app/assets/javascripts/todos.js index e9513725d9d..caaf6484a34 100644 --- a/app/assets/javascripts/todos.js +++ b/app/assets/javascripts/todos.js @@ -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) => { - class Todos { - constructor() { - this.initFilters(); - this.bindEvents(); +class Todos { + constructor() { + this.initFilters(); + this.bindEvents(); - this.cleanupWrapper = this.cleanup.bind(this); - document.addEventListener('beforeunload', this.cleanupWrapper); - } + this.cleanupWrapper = this.cleanup.bind(this); + document.addEventListener('beforeunload', this.cleanupWrapper); + } - cleanup() { - this.unbindEvents(); - document.removeEventListener('beforeunload', this.cleanupWrapper); - } + cleanup() { + this.unbindEvents(); + document.removeEventListener('beforeunload', this.cleanupWrapper); + } - unbindEvents() { - $('.js-done-todo, .js-undo-todo').off('click', this.updateStateClickedWrapper); - $('.js-todos-mark-all').off('click', this.allDoneClickedWrapper); - $('.todo').off('click', this.goToTodoUrl); - } + unbindEvents() { + $('.js-done-todo, .js-undo-todo, .js-add-todo').off('click', this.updateRowStateClickedWrapper); + $('.js-todos-mark-all').off('click', this.allDoneClickedWrapper); + $('.todo').off('click', this.goToTodoUrl); + } - bindEvents() { - this.updateStateClickedWrapper = this.updateStateClicked.bind(this); - this.allDoneClickedWrapper = this.allDoneClicked.bind(this); + bindEvents() { + this.updateRowStateClickedWrapper = this.updateRowStateClicked.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); - } + $('.js-done-todo, .js-undo-todo, .js-add-todo').on('click', this.updateRowStateClickedWrapper); + $('.js-todos-mark-all').on('click', this.allDoneClickedWrapper); + $('.todo').on('click', this.goToTodoUrl); + } - initFilters() { - new UsersSelect(); - this.initFilterDropdown($('.js-project-search'), 'project_id', ['text']); - this.initFilterDropdown($('.js-type-search'), 'type'); - this.initFilterDropdown($('.js-action-search'), 'action_id'); + initFilters() { + this.initFilterDropdown($('.js-project-search'), 'project_id', ['text']); + this.initFilterDropdown($('.js-type-search'), 'type'); + this.initFilterDropdown($('.js-action-search'), 'action_id'); - $('form.filter-form').on('submit', function (event) { - event.preventDefault(); - gl.utils.visitUrl(`${this.action}&${$(this).serialize()}`); - }); - } + $('form.filter-form').on('submit', function applyFilters(event) { + event.preventDefault(); + gl.utils.visitUrl(`${this.action}&${$(this).serialize()}`); + }); + return new UsersSelect(); + } - initFilterDropdown($dropdown, fieldName, searchFields) { - $dropdown.glDropdown({ - fieldName, - selectable: true, - filterable: searchFields ? true : false, - search: { fields: searchFields }, - data: $dropdown.data('data'), - clicked: function () { - return $dropdown.closest('form.filter-form').submit(); - }, - }); - } + initFilterDropdown($dropdown, fieldName, searchFields) { + $dropdown.glDropdown({ + fieldName, + selectable: true, + filterable: searchFields ? true : false, + search: { fields: searchFields }, + data: $dropdown.data('data'), + clicked: () => $dropdown.closest('form.filter-form').submit(), + }); + } - updateStateClicked(e) { - 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); - }, - }); - } + updateRowStateClicked(e) { + e.preventDefault(); - 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('
You\'re all done!
'); - this.updateBadges(data); - }, - }); - } + 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.updateRowState(target); + return this.updateBadges(data); + }, + }); + } - updateState(target) { - const row = target.closest('li'); - const restoreBtn = row.querySelector('.js-undo-todo'); - const doneBtn = row.querySelector('.js-done-todo'); + 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('
You\'re all done!
'); + this.updateBadges(data); + }, + }); + } - target.removeAttribute('disabled'); - target.classList.remove('disabled'); - target.classList.add('hidden'); + updateRowState(target) { + const row = target.closest('li'); + const restoreBtn = row.querySelector('.js-undo-todo'); + const doneBtn = row.querySelector('.js-done-todo'); - if (target === doneBtn) { - row.classList.add('done-reversible'); - restoreBtn.classList.remove('hidden'); - } else { - row.classList.remove('done-reversible'); - doneBtn.classList.remove('hidden'); - } - } + target.classList.add('hidden'); + target.removeAttribute('disabled'); + target.classList.remove('disabled'); - updateBadges(data) { - $(document).trigger('todo:toggle', data.count); - $('.todos-pending .badge').text(data.count); - $('.todos-done .badge').text(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); - } + if (target === doneBtn) { + row.classList.add('done-reversible'); + restoreBtn.classList.remove('hidden'); + } else if (target === restoreBtn) { + row.classList.remove('done-reversible'); + doneBtn.classList.remove('hidden'); + } else { + row.parentNode.removeChild(row); } } - global.Todos = Todos; -})(window.gl || (window.gl = {})); + updateBadges(data) { + $(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; diff --git a/app/views/dashboard/todos/_todo.html.haml b/app/views/dashboard/todos/_todo.html.haml index a3993d5ef16..388190642aa 100644 --- a/app/views/dashboard/todos/_todo.html.haml +++ b/app/views/dashboard/todos/_todo.html.haml @@ -42,3 +42,8 @@ = link_to restore_dashboard_todo_path(todo), method: :patch, class: 'btn btn-loading js-undo-todo hidden' 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 + Add todo + = icon('spinner spin') diff --git a/changelogs/unreleased/27114-add-undo-to-todos-in-the-done-tab.yml b/changelogs/unreleased/27114-add-undo-to-todos-in-the-done-tab.yml new file mode 100644 index 00000000000..2e6c10a6bfe --- /dev/null +++ b/changelogs/unreleased/27114-add-undo-to-todos-in-the-done-tab.yml @@ -0,0 +1,4 @@ +--- +title: Add Undo to Todos in the Done tab +merge_request: 8782 +author: Jacopo Beschi @jacopo-beschi diff --git a/spec/features/todos/todos_spec.rb b/spec/features/todos/todos_spec.rb index 3495091a0d5..5c2df949ac5 100644 --- a/spec/features/todos/todos_spec.rb +++ b/spec/features/todos/todos_spec.rb @@ -38,7 +38,9 @@ describe 'Dashboard Todos', feature: true do shared_examples 'deleting the todo' do before do - first('.js-done-todo').click + within first('.todo') do + click_link 'Done' + end end 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 before do - first('.js-done-todo').click - wait_for_ajax - first('.js-undo-todo').click + within first('.todo') do + click_link 'Done' + wait_for_ajax + click_link 'Undo' + end end it 'is marked back as pending in the list' do @@ -97,6 +101,35 @@ describe 'Dashboard Todos', feature: true do 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 before do label1 = create(:label, project: project) @@ -143,7 +176,7 @@ describe 'Dashboard Todos', feature: true do describe 'mark all as done', js: true do before do visit dashboard_todos_path - click_link('Mark all as done') + click_link 'Mark all as done' end it 'shows "All done" message!' do