diff options
20 files changed, 268 insertions, 63 deletions
diff --git a/app/assets/javascripts/todos.js b/app/assets/javascripts/todos.js index caaf6484a34..8be58023c84 100644 --- a/app/assets/javascripts/todos.js +++ b/app/assets/javascripts/todos.js @@ -5,6 +5,7 @@ class Todos { constructor() { this.initFilters(); this.bindEvents(); + this.todo_ids = []; this.cleanupWrapper = this.cleanup.bind(this); document.addEventListener('beforeunload', this.cleanupWrapper); @@ -17,16 +18,16 @@ class Todos { unbindEvents() { $('.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); } bindEvents() { 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-todos-mark-all').on('click', this.allDoneClickedWrapper); + $('.js-todos-mark-all, .js-todos-undo-all').on('click', this.updateAllStateClickedWrapper); $('.todo').on('click', this.goToTodoUrl); } @@ -57,14 +58,14 @@ class Todos { e.preventDefault(); const target = e.target; - target.setAttribute('disabled', ''); + target.setAttribute('disabled', true); target.classList.add('disabled'); $.ajax({ type: 'POST', - url: target.getAttribute('href'), + url: target.dataset.href, dataType: 'json', data: { - '_method': target.getAttribute('data-method'), + '_method': target.dataset.method, }, success: (data) => { 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) { const row = target.closest('li'); 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) { $(document).trigger('todo:toggle', data.count); document.querySelector('.todos-pending .badge').innerHTML = data.count; diff --git a/app/controllers/dashboard/todos_controller.rb b/app/controllers/dashboard/todos_controller.rb index 5848ca62777..498690e8f11 100644 --- a/app/controllers/dashboard/todos_controller.rb +++ b/app/controllers/dashboard/todos_controller.rb @@ -22,12 +22,12 @@ class Dashboard::TodosController < Dashboard::ApplicationController end 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| format.html { redirect_to dashboard_todos_path, notice: 'All todos were marked as done.' } format.js { head :ok } - format.json { render json: todos_counts } + format.json { render json: todos_counts.merge(updated_ids: updated_ids) } end end @@ -37,6 +37,12 @@ class Dashboard::TodosController < Dashboard::ApplicationController render json: todos_counts 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 def self.todos_count_format(count) count >= 100 ? '99+' : count diff --git a/app/helpers/milestones_helper.rb b/app/helpers/milestones_helper.rb index 7011e670cee..5053b937c02 100644 --- a/app/helpers/milestones_helper.rb +++ b/app/helpers/milestones_helper.rb @@ -82,12 +82,13 @@ module MilestonesHelper def milestone_remaining_days(milestone) if milestone.expired? content_tag(:strong, 'Past due') - elsif milestone.due_date - days = milestone.remaining_days - content = content_tag(:strong, days) - content << " #{'day'.pluralize(days)} remaining" elsif milestone.upcoming? content_tag(:strong, 'Upcoming') + elsif milestone.due_date + time_ago = time_ago_in_words(milestone.due_date) + content = time_ago.gsub(/\d+/) { |match| "<strong>#{match}</strong>" } + content.slice!("about ") + content << " remaining" elsif milestone.start_date && milestone.start_date.past? days = milestone.elapsed_days content = content_tag(:strong, days) diff --git a/app/services/todo_service.rb b/app/services/todo_service.rb index 8787a1c93a9..bf7e76ec59e 100644 --- a/app/services/todo_service.rb +++ b/app/services/todo_service.rb @@ -201,10 +201,12 @@ class TodoService 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) + # Only update those that are not really on that state + todos = todos.where.not(state: state) + todos_ids = todos.pluck(:id) + todos.update_all(state: state) current_user.update_todos_count_cache - marked_todos + todos_ids end def create_todos(users, attributes) diff --git a/app/views/dashboard/todos/_todo.html.haml b/app/views/dashboard/todos/_todo.html.haml index 388190642aa..d0c12aa57ae 100644 --- a/app/views/dashboard/todos/_todo.html.haml +++ b/app/views/dashboard/todos/_todo.html.haml @@ -36,14 +36,14 @@ - if todo.pending? .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 = 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 = icon('spinner spin') - else .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 = icon('spinner spin') diff --git a/app/views/dashboard/todos/index.html.haml b/app/views/dashboard/todos/index.html.haml index 3ed67d9258c..d31ced004a0 100644 --- a/app/views/dashboard/todos/index.html.haml +++ b/app/views/dashboard/todos/index.html.haml @@ -19,9 +19,12 @@ .nav-controls - 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 = 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 .row-content-block.second-block @@ -67,12 +70,16 @@ .js-todos-all - if @todos.any? - .js-todos-options{ data: {per_page: @todos.limit_value, current_page: @todos.current_page, total_pages: @todos.total_pages} } - .panel.panel-default.panel-small.panel-without-border - %ul.content-list.todos-list - = render @todos - = paginate @todos, theme: "gitlab" - + .js-todos-list-container + .js-todos-options{ data: { per_page: @todos.limit_value, current_page: @todos.current_page, total_pages: @todos.total_pages } } + .panel.panel-default.panel-small.panel-without-border + %ul.content-list.todos-list + = 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? .todos-all-done = render "shared/empty_states/icons/todos_all_done.svg" diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml index 2a98bba05ee..d129da943f8 100644 --- a/app/views/projects/new.html.haml +++ b/app/views/projects/new.html.haml @@ -1,5 +1,6 @@ - page_title 'New Project' - header_title "Projects", dashboard_projects_path +- visibility_level = params.dig(:project, :visibility_level) || default_project_visibility .project-edit-container .project-edit-errors @@ -95,7 +96,7 @@ = f.label :visibility_level, class: 'label-light' do Visibility Level = link_to icon('question-circle'), help_page_path("public_access/public_access") - = render 'shared/visibility_level', f: f, visibility_level: default_project_visibility, can_change_visibility_level: true, form_model: @project, with_label: false + = render 'shared/visibility_level', f: f, visibility_level: visibility_level.to_i, can_change_visibility_level: true, form_model: @project, with_label: false = f.submit 'Create project', class: "btn btn-create project-submit", tabindex: 4 = link_to 'Cancel', dashboard_projects_path, class: 'btn btn-cancel' diff --git a/changelogs/unreleased/27114-add-undo-mark-all-as-done-to-todos.yml b/changelogs/unreleased/27114-add-undo-mark-all-as-done-to-todos.yml new file mode 100644 index 00000000000..44aae486574 --- /dev/null +++ b/changelogs/unreleased/27114-add-undo-mark-all-as-done-to-todos.yml @@ -0,0 +1,4 @@ +--- +title: Add Undo mark all as done to Todos +merge_request: 9890 +author: Jacopo Beschi @jacopo-beschi diff --git a/changelogs/unreleased/chore-23493-remaining-time-tooltip.yml b/changelogs/unreleased/chore-23493-remaining-time-tooltip.yml new file mode 100644 index 00000000000..dc315ca2367 --- /dev/null +++ b/changelogs/unreleased/chore-23493-remaining-time-tooltip.yml @@ -0,0 +1,5 @@ +--- +title: Added remaining_time method to milestoneish, specs and updated the milestone_helper + milestone_remaining_days method to correctly return the correct remaining time. +merge_request: +author: Michael Robinson diff --git a/changelogs/unreleased/fix_visibility_level.yml b/changelogs/unreleased/fix_visibility_level.yml new file mode 100644 index 00000000000..4cf649124ca --- /dev/null +++ b/changelogs/unreleased/fix_visibility_level.yml @@ -0,0 +1,4 @@ +--- +title: Fix visibility level on new project page +merge_request: 9885 +author: blackst0ne diff --git a/config/routes/dashboard.rb b/config/routes/dashboard.rb index adc3ad207cc..8e380a0b0ac 100644 --- a/config/routes/dashboard.rb +++ b/config/routes/dashboard.rb @@ -13,6 +13,7 @@ resource :dashboard, controller: 'dashboard', only: [] do resources :todos, only: [:index, :destroy] do collection do delete :destroy_all + patch :bulk_restore end member do patch :restore diff --git a/db/post_migrate/20170306170512_migrate_legacy_manual_actions.rb b/db/post_migrate/20170306170512_migrate_legacy_manual_actions.rb index 9020e0d054c..ec6e8cdfc45 100644 --- a/db/post_migrate/20170306170512_migrate_legacy_manual_actions.rb +++ b/db/post_migrate/20170306170512_migrate_legacy_manual_actions.rb @@ -4,6 +4,8 @@ class MigrateLegacyManualActions < ActiveRecord::Migration DOWNTIME = false def up + disable_statement_timeout + execute <<-EOS UPDATE ci_builds SET status = 'manual', allow_failure = true WHERE ci_builds.when = 'manual' AND ci_builds.status = 'skipped'; @@ -11,6 +13,8 @@ class MigrateLegacyManualActions < ActiveRecord::Migration end def down + disable_statement_timeout + execute <<-EOS UPDATE ci_builds SET status = 'skipped', allow_failure = false WHERE ci_builds.when = 'manual' AND ci_builds.status = 'manual'; diff --git a/features/steps/dashboard/todos.rb b/features/steps/dashboard/todos.rb index eb906a55a83..9f01dff776f 100644 --- a/features/steps/dashboard/todos.rb +++ b/features/steps/dashboard/todos.rb @@ -159,7 +159,11 @@ class Spinach::Features::DashboardTodos < Spinach::FeatureSteps end 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 def john_doe diff --git a/lib/api/v3/todos.rb b/lib/api/v3/todos.rb index e60cb25e57b..e3b311d61cd 100644 --- a/lib/api/v3/todos.rb +++ b/lib/api/v3/todos.rb @@ -20,9 +20,9 @@ module API desc 'Mark all todos as done' delete do status(200) - + 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 diff --git a/spec/controllers/dashboard/todos_controller_spec.rb b/spec/controllers/dashboard/todos_controller_spec.rb index 7072bd5e87c..71a4a2c43c7 100644 --- a/spec/controllers/dashboard/todos_controller_spec.rb +++ b/spec/controllers/dashboard/todos_controller_spec.rb @@ -49,4 +49,18 @@ describe Dashboard::TodosController do expect(json_response).to eq({ "count" => "1", "done_count" => "0" }) 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 diff --git a/spec/features/projects/new_project_spec.rb b/spec/features/projects/new_project_spec.rb index 45185f2dd1f..52196ce49bd 100644 --- a/spec/features/projects/new_project_spec.rb +++ b/spec/features/projects/new_project_spec.rb @@ -16,6 +16,15 @@ feature "New project", feature: true do expect(find_field("project_visibility_level_#{level}")).to be_checked end + + it 'saves visibility level on validation error' do + visit new_project_path + + choose(key) + click_button('Create project') + + expect(find_field("project_visibility_level_#{level}")).to be_checked + end end end diff --git a/spec/features/todos/todos_spec.rb b/spec/features/todos/todos_spec.rb index 5c2df949ac5..850020109d4 100644 --- a/spec/features/todos/todos_spec.rb +++ b/spec/features/todos/todos_spec.rb @@ -31,7 +31,7 @@ describe 'Dashboard Todos', feature: true do end it 'shows due date as today' do - page.within first('.todo') do + within first('.todo') do expect(page).to have_content 'Due today' end end @@ -184,6 +184,60 @@ describe 'Dashboard Todos', feature: true do expect(page).to have_content "You're all done!" expect(page).not_to have_selector('.gl-pagination') 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 diff --git a/spec/helpers/milestones_helper_spec.rb b/spec/helpers/milestones_helper_spec.rb index 68b20a1e4fc..77a4ba305bb 100644 --- a/spec/helpers/milestones_helper_spec.rb +++ b/spec/helpers/milestones_helper_spec.rb @@ -47,4 +47,54 @@ describe MilestonesHelper do end end end + + describe '#milestone_remaining_days' do + context 'when less than 31 days remaining' do + let(:milestone_remaining) { milestone_remaining_days(build_stubbed(:milestone, due_date: 12.days.from_now)) } + + it 'returns days remaining' do + expect(milestone_remaining).to eq("<strong>11</strong> days remaining") + end + end + + context 'when less than 1 year and more than 30 days remaining' do + let(:milestone_remaining) { milestone_remaining_days(build_stubbed(:milestone, due_date: 2.months.from_now)) } + + it 'returns months remaining' do + expect(milestone_remaining).to eq("<strong>2</strong> months remaining") + end + end + + context 'when more than 1 year remaining' do + let(:milestone_remaining) { milestone_remaining_days(build_stubbed(:milestone, due_date: 1.year.from_now + 2.days)) } + + it 'returns years remaining' do + expect(milestone_remaining).to eq("<strong>1</strong> year remaining") + end + end + + context 'when milestone is expired' do + let(:milestone_remaining) { milestone_remaining_days(build_stubbed(:milestone, due_date: 2.days.ago)) } + + it 'returns "Past due"' do + expect(milestone_remaining).to eq("<strong>Past due</strong>") + end + end + + context 'when milestone has start_date in the future' do + let(:milestone_remaining) { milestone_remaining_days(build_stubbed(:milestone, start_date: 2.days.from_now)) } + + it 'returns "Upcoming"' do + expect(milestone_remaining).to eq("<strong>Upcoming</strong>") + end + end + + context 'when milestone has start_date in the past' do + let(:milestone_remaining) { milestone_remaining_days(build_stubbed(:milestone, start_date: 2.days.ago)) } + + it 'returns days elapsed' do + expect(milestone_remaining).to eq("<strong>2</strong> days elapsed") + end + end + end end diff --git a/spec/models/concerns/milestoneish_spec.rb b/spec/models/concerns/milestoneish_spec.rb index ad703a6c8bb..68e4c0a522b 100644 --- a/spec/models/concerns/milestoneish_spec.rb +++ b/spec/models/concerns/milestoneish_spec.rb @@ -116,21 +116,41 @@ describe Milestone, 'Milestoneish' do end end + describe '#remaining_days' do + it 'shows 0 if no due date' do + milestone = build_stubbed(:milestone) + + expect(milestone.remaining_days).to eq(0) + end + + it 'shows 0 if expired' do + milestone = build_stubbed(:milestone, due_date: 2.days.ago) + + expect(milestone.remaining_days).to eq(0) + end + + it 'shows correct remaining days' do + milestone = build_stubbed(:milestone, due_date: 2.days.from_now) + + expect(milestone.remaining_days).to eq(2) + end + end + describe '#elapsed_days' do it 'shows 0 if no start_date set' do - milestone = build(:milestone) + milestone = build_stubbed(:milestone) expect(milestone.elapsed_days).to eq(0) end it 'shows 0 if start_date is a future' do - milestone = build(:milestone, start_date: Time.now + 2.days) + milestone = build_stubbed(:milestone, start_date: Time.now + 2.days) expect(milestone.elapsed_days).to eq(0) end it 'shows correct amount of days' do - milestone = build(:milestone, start_date: Time.now - 2.days) + milestone = build_stubbed(:milestone, start_date: Time.now - 2.days) expect(milestone.elapsed_days).to eq(2) end diff --git a/spec/services/todo_service_spec.rb b/spec/services/todo_service_spec.rb index a8395cb48ea..3645b73b039 100644 --- a/spec/services/todo_service_spec.rb +++ b/spec/services/todo_service_spec.rb @@ -298,6 +298,10 @@ describe TodoService, services: true do expect(second_todo.reload.state?(new_state)).to be true 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 it 'updates when todos change' do 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) end end - + describe '#mark_todo' do it 'creates a todo from a merge request' do service.mark_todo(mr_unassigned, author) @@ -779,29 +783,27 @@ describe TodoService, services: true do .to change { todo.reload.state }.from('pending').to('done') 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) - 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 context 'when some of the todos are done already' do - before do - create(:todo, :mentioned, user: john_doe, target: issue, project: project) - create(:todo, :mentioned, user: john_doe, target: another_issue, project: project) - end + let!(:first_todo) { create(:todo, :mentioned, user: john_doe, target: issue, project: project) } + let!(:second_todo) { create(:todo, :mentioned, user: john_doe, target: another_issue, project: project) } - 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) - 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 - 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(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 |