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