Commit c45e11f1 authored by Jacob Schatz's avatar Jacob Schatz

Merge branch 'async-milestone-tabs' into 'master'

Load milestone tabs asynchronously

See merge request !10919
parents 349e4231 471888d6
...@@ -19,12 +19,10 @@ ...@@ -19,12 +19,10 @@
}); });
}; };
Milestone.sortIssues = function(data) { Milestone.sortIssues = function(url, data) {
var sort_issues_url;
sort_issues_url = location.href + "/sort_issues";
return $.ajax({ return $.ajax({
type: "PUT", type: "PUT",
url: sort_issues_url, url,
data: data, data: data,
success: function(_data) { success: function(_data) {
return Milestone.successCallback(_data); return Milestone.successCallback(_data);
...@@ -36,12 +34,10 @@ ...@@ -36,12 +34,10 @@
}); });
}; };
Milestone.sortMergeRequests = function(data) { Milestone.sortMergeRequests = function(url, data) {
var sort_mr_url;
sort_mr_url = location.href + "/sort_merge_requests";
return $.ajax({ return $.ajax({
type: "PUT", type: "PUT",
url: sort_mr_url, url,
data: data, data: data,
success: function(_data) { success: function(_data) {
return Milestone.successCallback(_data); return Milestone.successCallback(_data);
...@@ -81,42 +77,55 @@ ...@@ -81,42 +77,55 @@
}; };
function Milestone() { function Milestone() {
var oldMouseStart; this.issuesSortEndpoint = $('#tab-issues').data('sort-endpoint');
this.mergeRequestsSortEndpoint = $('#tab-merge-requests').data('sort-endpoint');
this.bindIssuesSorting(); this.bindIssuesSorting();
this.bindMergeRequestSorting();
this.bindTabsSwitching(); this.bindTabsSwitching();
// Load merge request tab if it is active
// merge request tab is active based on different conditions in the backend
this.loadTab($('.js-milestone-tabs .active a'));
this.loadInitialTab();
} }
Milestone.prototype.bindIssuesSorting = function() { Milestone.prototype.bindIssuesSorting = function() {
if (!this.issuesSortEndpoint) return;
$('#issues-list-unassigned, #issues-list-ongoing, #issues-list-closed').each(function (i, el) { $('#issues-list-unassigned, #issues-list-ongoing, #issues-list-closed').each(function (i, el) {
this.createSortable(el, { this.createSortable(el, {
group: 'issue-list', group: 'issue-list',
listEls: $('.issues-sortable-list'), listEls: $('.issues-sortable-list'),
fieldName: 'issue', fieldName: 'issue',
sortCallback: Milestone.sortIssues, sortCallback: (data) => {
Milestone.sortIssues(this.issuesSortEndpoint, data);
},
updateCallback: Milestone.updateIssue, updateCallback: Milestone.updateIssue,
}); });
}.bind(this)); }.bind(this));
}; };
Milestone.prototype.bindTabsSwitching = function() { Milestone.prototype.bindTabsSwitching = function() {
return $('a[data-toggle="tab"]').on('show.bs.tab', function(e) { return $('a[data-toggle="tab"]').on('show.bs.tab', (e) => {
var currentTabClass, previousTabClass; const $target = $(e.target);
currentTabClass = $(e.target).data('show');
previousTabClass = $(e.relatedTarget).data('show'); location.hash = $target.attr('href');
$(previousTabClass).hide(); this.loadTab($target);
$(currentTabClass).removeClass('hidden');
return $(currentTabClass).show();
}); });
}; };
Milestone.prototype.bindMergeRequestSorting = function() { Milestone.prototype.bindMergeRequestSorting = function() {
if (!this.mergeRequestsSortEndpoint) return;
$("#merge_requests-list-unassigned, #merge_requests-list-ongoing, #merge_requests-list-closed").each(function (i, el) { $("#merge_requests-list-unassigned, #merge_requests-list-ongoing, #merge_requests-list-closed").each(function (i, el) {
this.createSortable(el, { this.createSortable(el, {
group: 'merge-request-list', group: 'merge-request-list',
listEls: $(".merge_requests-sortable-list:not(#merge_requests-list-merged)"), listEls: $(".merge_requests-sortable-list:not(#merge_requests-list-merged)"),
fieldName: 'merge_request', fieldName: 'merge_request',
sortCallback: Milestone.sortMergeRequests, sortCallback: (data) => {
Milestone.sortMergeRequests(this.mergeRequestsSortEndpoint, data);
},
updateCallback: Milestone.updateMergeRequest, updateCallback: Milestone.updateMergeRequest,
}); });
}.bind(this)); }.bind(this));
...@@ -169,6 +178,35 @@ ...@@ -169,6 +178,35 @@
}); });
}; };
Milestone.prototype.loadInitialTab = function() {
const $target = $(`.js-milestone-tabs a[href="${location.hash}"]`);
if ($target.length) {
$target.tab('show');
}
};
Milestone.prototype.loadTab = function($target) {
const endpoint = $target.data('endpoint');
const tabElId = $target.attr('href');
if (endpoint && !$target.hasClass('is-loaded')) {
$.ajax({
url: endpoint,
dataType: 'JSON',
})
.fail(() => new Flash('Error loading milestone tab'))
.done((data) => {
$(tabElId).html(data.html);
$target.addClass('is-loaded');
if (tabElId === '#tab-merge-requests') {
this.bindMergeRequestSorting();
}
});
}
};
return Milestone; return Milestone;
})(); })();
}).call(window); }).call(window);
module MilestoneActions
extend ActiveSupport::Concern
def merge_requests
respond_to do |format|
format.html { redirect_to milestone_redirect_path }
format.json do
render json: tabs_json("shared/milestones/_merge_requests_tab", {
merge_requests: @milestone.merge_requests,
show_project_name: true
})
end
end
end
def participants
respond_to do |format|
format.html { redirect_to milestone_redirect_path }
format.json do
render json: tabs_json("shared/milestones/_participants_tab", {
users: @milestone.participants
})
end
end
end
def labels
respond_to do |format|
format.html { redirect_to milestone_redirect_path }
format.json do
render json: tabs_json("shared/milestones/_labels_tab", {
labels: @milestone.labels
})
end
end
end
private
def tabs_json(partial, data = {})
{
html: view_to_html_string(partial, data)
}
end
def milestone_redirect_path
if @project
namespace_project_milestone_path(@project.namespace, @project, @milestone)
else
group_milestone_path(@group, @milestone.safe_title, title: @milestone.title)
end
end
end
class Groups::MilestonesController < Groups::ApplicationController class Groups::MilestonesController < Groups::ApplicationController
include MilestoneActions
before_action :group_projects before_action :group_projects
before_action :milestone, only: [:show, :update] before_action :milestone, only: [:show, :update, :merge_requests, :participants, :labels]
before_action :authorize_admin_milestones!, only: [:new, :create, :update] before_action :authorize_admin_milestones!, only: [:new, :create, :update]
def index def index
......
class Projects::MilestonesController < Projects::ApplicationController class Projects::MilestonesController < Projects::ApplicationController
include MilestoneActions
before_action :module_enabled before_action :module_enabled
before_action :milestone, only: [:edit, :update, :destroy, :show, :sort_issues, :sort_merge_requests] before_action :milestone, only: [:edit, :update, :destroy, :show, :sort_issues, :sort_merge_requests, :merge_requests, :participants, :labels]
# Allow read any milestone # Allow read any milestone
before_action :authorize_read_milestone! before_action :authorize_read_milestone!
# Allow admin milestone # Allow admin milestone
before_action :authorize_admin_milestone!, except: [:index, :show] before_action :authorize_admin_milestone!, except: [:index, :show, :merge_requests, :participants, :labels]
respond_to :html respond_to :html
......
...@@ -115,4 +115,28 @@ module MilestonesHelper ...@@ -115,4 +115,28 @@ module MilestonesHelper
end end
end end
end end
def milestone_merge_request_tab_path(milestone)
if @project
merge_requests_namespace_project_milestone_path(@project.namespace, @project, milestone, format: :json)
elsif @group
merge_requests_group_milestone_path(@group, milestone.safe_title, title: milestone.title, format: :json)
end
end
def milestone_participants_tab_path(milestone)
if @project
participants_namespace_project_milestone_path(@project.namespace, @project, milestone, format: :json)
elsif @group
participants_group_milestone_path(@group, milestone.safe_title, title: milestone.title, format: :json)
end
end
def milestone_labels_tab_path(milestone)
if @project
labels_namespace_project_milestone_path(@project.namespace, @project, milestone, format: :json)
elsif @group
labels_group_milestone_path(@group, milestone.safe_title, title: milestone.title, format: :json)
end
end
end end
.text-center.prepend-top-default
= icon('spin spinner 2x', 'aria-hidden': 'true', 'aria-label': 'Loading tab content')
.scrolling-tabs-container.inner-page-scroll-tabs.is-smaller .scrolling-tabs-container.inner-page-scroll-tabs.is-smaller
.fade-left= icon('angle-left') .fade-left= icon('angle-left')
.fade-right= icon('angle-right') .fade-right= icon('angle-right')
%ul.nav-links.scrolling-tabs %ul.nav-links.scrolling-tabs.js-milestone-tabs
- if milestone.is_a?(GlobalMilestone) || can?(current_user, :read_issue, @project) - if milestone.is_a?(GlobalMilestone) || can?(current_user, :read_issue, @project)
%li.active %li.active
= link_to '#tab-issues', 'data-toggle' => 'tab', 'data-show' => '.tab-issues-buttons' do = link_to '#tab-issues', 'data-toggle' => 'tab', 'data-show' => '.tab-issues-buttons' do
Issues Issues
%span.badge= milestone.issues_visible_to_user(current_user).size %span.badge= milestone.issues_visible_to_user(current_user).size
%li %li
= link_to '#tab-merge-requests', 'data-toggle' => 'tab', 'data-show' => '.tab-merge-requests-buttons' do = link_to '#tab-merge-requests', 'data-toggle' => 'tab', 'data-endpoint': milestone_merge_request_tab_path(milestone) do
Merge Requests Merge Requests
%span.badge= milestone.merge_requests.size %span.badge= milestone.merge_requests.size
- else - else
%li.active %li.active
= link_to '#tab-merge-requests', 'data-toggle' => 'tab', 'data-show' => '.tab-merge-requests-buttons' do = link_to '#tab-merge-requests', 'data-toggle' => 'tab', 'data-endpoint': milestone_merge_request_tab_path(milestone) do
Merge Requests Merge Requests
%span.badge= milestone.merge_requests.size %span.badge= milestone.merge_requests.size
%li %li
= link_to '#tab-participants', 'data-toggle' => 'tab' do = link_to '#tab-participants', 'data-toggle' => 'tab', 'data-endpoint': milestone_participants_tab_path(milestone) do
Participants Participants
%span.badge= milestone.participants.count %span.badge= milestone.participants.count
%li %li
= link_to '#tab-labels', 'data-toggle' => 'tab' do = link_to '#tab-labels', 'data-toggle' => 'tab', 'data-endpoint': milestone_labels_tab_path(milestone) do
Labels Labels
%span.badge= milestone.labels.count %span.badge= milestone.labels.count
...@@ -30,14 +30,18 @@ ...@@ -30,14 +30,18 @@
.tab-content.milestone-content .tab-content.milestone-content
- if milestone.is_a?(GlobalMilestone) || can?(current_user, :read_issue, @project) - if milestone.is_a?(GlobalMilestone) || can?(current_user, :read_issue, @project)
.tab-pane.active#tab-issues .tab-pane.active#tab-issues{ data: { sort_endpoint: (sort_issues_namespace_project_milestone_path(@project.namespace, @project, @milestone) if @project && current_user) } }
= render 'shared/milestones/issues_tab', issues: milestone.issues_visible_to_user(current_user).include_associations, show_project_name: show_project_name, show_full_project_name: show_full_project_name = render 'shared/milestones/issues_tab', issues: milestone.issues_visible_to_user(current_user).include_associations, show_project_name: show_project_name, show_full_project_name: show_full_project_name
.tab-pane#tab-merge-requests .tab-pane#tab-merge-requests{ data: { sort_endpoint: (sort_merge_requests_namespace_project_milestone_path(@project.namespace, @project, @milestone) if @project && current_user) } }
= render 'shared/milestones/merge_requests_tab', merge_requests: milestone.merge_requests, show_project_name: show_project_name, show_full_project_name: show_full_project_name -# loaded async
= render "shared/milestones/tab_loading"
- else - else
.tab-pane.active#tab-merge-requests .tab-pane.active#tab-merge-requests{ data: { sort_endpoint: (sort_merge_requests_namespace_project_milestone_path(@project.namespace, @project, @milestone) if @project && current_user) } }
= render 'shared/milestones/merge_requests_tab', merge_requests: milestone.merge_requests, show_project_name: show_project_name, show_full_project_name: show_full_project_name -# loaded async
= render "shared/milestones/tab_loading"
.tab-pane#tab-participants .tab-pane#tab-participants
= render 'shared/milestones/participants_tab', users: milestone.participants -# loaded async
= render "shared/milestones/tab_loading"
.tab-pane#tab-labels .tab-pane#tab-labels
= render 'shared/milestones/labels_tab', labels: milestone.labels -# loaded async
= render "shared/milestones/tab_loading"
---
title: Load milestone tabs asynchronously to increase initial load performance
merge_request:
author:
...@@ -10,7 +10,13 @@ scope(path: 'groups/*group_id', ...@@ -10,7 +10,13 @@ scope(path: 'groups/*group_id',
end end
resource :avatar, only: [:destroy] resource :avatar, only: [:destroy]
resources :milestones, constraints: { id: /[^\/]+/ }, only: [:index, :show, :update, :new, :create] resources :milestones, constraints: { id: /[^\/]+/ }, only: [:index, :show, :update, :new, :create] do
member do
get :merge_requests
get :participants
get :labels
end
end
resources :labels, except: [:show] do resources :labels, except: [:show] do
post :toggle_subscription, on: :member post :toggle_subscription, on: :member
......
...@@ -207,6 +207,9 @@ constraints(ProjectUrlConstrainer.new) do ...@@ -207,6 +207,9 @@ constraints(ProjectUrlConstrainer.new) do
member do member do
put :sort_issues put :sort_issues
put :sort_merge_requests put :sort_merge_requests
get :merge_requests
get :participants
get :labels
end end
end end
......
...@@ -38,6 +38,7 @@ Feature: Group Milestones ...@@ -38,6 +38,7 @@ Feature: Group Milestones
And I should see the "feature" label And I should see the "feature" label
And I should see the project name in the Issue row And I should see the project name in the Issue row
@javascript
Scenario: I should see the Labels tab Scenario: I should see the Labels tab
Given Group has projects with milestones Given Group has projects with milestones
When I visit group "Owned" page When I visit group "Owned" page
......
...@@ -7,14 +7,6 @@ Feature: Project Milestone ...@@ -7,14 +7,6 @@ Feature: Project Milestone
And milestone has issue "Bugfix1" with labels: "bug", "feature" And milestone has issue "Bugfix1" with labels: "bug", "feature"
And milestone has issue "Bugfix2" with labels: "bug", "enhancement" And milestone has issue "Bugfix2" with labels: "bug", "enhancement"
@javascript
Scenario: Listing issues from issues tab
Given I visit project "Shop" milestones page
And I click link "v2.2"
Then I should see the labels "bug", "enhancement" and "feature"
And I should see the "bug" label listed only once
@javascript @javascript
Scenario: Listing labels from labels tab Scenario: Listing labels from labels tab
Given I visit project "Shop" milestones page Given I visit project "Shop" milestones page
......
class Spinach::Features::GroupMilestones < Spinach::FeatureSteps class Spinach::Features::GroupMilestones < Spinach::FeatureSteps
include WaitForAjax
include SharedAuthentication include SharedAuthentication
include SharedPaths include SharedPaths
include SharedGroup include SharedGroup
...@@ -90,6 +91,8 @@ class Spinach::Features::GroupMilestones < Spinach::FeatureSteps ...@@ -90,6 +91,8 @@ class Spinach::Features::GroupMilestones < Spinach::FeatureSteps
end end
step 'I should see the list of labels' do step 'I should see the list of labels' do
wait_for_ajax
page.within('#tab-labels') do page.within('#tab-labels') do
expect(page).to have_content 'bug' expect(page).to have_content 'bug'
expect(page).to have_content 'feature' expect(page).to have_content 'feature'
......
...@@ -2,6 +2,7 @@ class Spinach::Features::ProjectMilestone < Spinach::FeatureSteps ...@@ -2,6 +2,7 @@ class Spinach::Features::ProjectMilestone < Spinach::FeatureSteps
include SharedAuthentication include SharedAuthentication
include SharedProject include SharedProject
include SharedPaths include SharedPaths
include WaitForAjax
step 'milestone has issue "Bugfix1" with labels: "bug", "feature"' do step 'milestone has issue "Bugfix1" with labels: "bug", "feature"' do
project = Project.find_by(name: "Shop") project = Project.find_by(name: "Shop")
...@@ -34,6 +35,8 @@ class Spinach::Features::ProjectMilestone < Spinach::FeatureSteps ...@@ -34,6 +35,8 @@ class Spinach::Features::ProjectMilestone < Spinach::FeatureSteps
end end
step 'I should see the labels "bug", "enhancement" and "feature"' do step 'I should see the labels "bug", "enhancement" and "feature"' do
wait_for_ajax
page.within('#tab-issues') do page.within('#tab-issues') do
expect(page).to have_content 'bug' expect(page).to have_content 'bug'
expect(page).to have_content 'enhancement' expect(page).to have_content 'enhancement'
......
...@@ -6,6 +6,16 @@ describe Groups::MilestonesController do ...@@ -6,6 +6,16 @@ describe Groups::MilestonesController do
let(:project2) { create(:empty_project, group: group) } let(:project2) { create(:empty_project, group: group) }
let(:user) { create(:user) } let(:user) { create(:user) }
let(:title) { '肯定不是中文的问题' } let(:title) { '肯定不是中文的问题' }
let(:milestone) do
project_milestone = create(:milestone, project: project)
GroupMilestone.build(
group,
[project],
project_milestone.title
)
end
let(:milestone_path) { group_milestone_path(group, milestone.safe_title, title: milestone.title) }
before do before do
sign_in(user) sign_in(user)
...@@ -14,6 +24,8 @@ describe Groups::MilestonesController do ...@@ -14,6 +24,8 @@ describe Groups::MilestonesController do
controller.instance_variable_set(:@group, group) controller.instance_variable_set(:@group, group)
end end
it_behaves_like 'milestone tabs'
describe "#create" do describe "#create" do
it "creates group milestone with Chinese title" do it "creates group milestone with Chinese title" do
post :create, post :create,
......
...@@ -7,6 +7,7 @@ describe Projects::MilestonesController do ...@@ -7,6 +7,7 @@ describe Projects::MilestonesController do
let(:issue) { create(:issue, project: project, milestone: milestone) } let(:issue) { create(:issue, project: project, milestone: milestone) }
let!(:label) { create(:label, project: project, title: 'Issue Label', issues: [issue]) } let!(:label) { create(:label, project: project, title: 'Issue Label', issues: [issue]) }
let!(:merge_request) { create(:merge_request, source_project: project, target_project: project, milestone: milestone) } let!(:merge_request) { create(:merge_request, source_project: project, target_project: project, milestone: milestone) }
let(:milestone_path) { namespace_project_milestone_path }
before do before do
sign_in(user) sign_in(user)
...@@ -14,6 +15,8 @@ describe Projects::MilestonesController do ...@@ -14,6 +15,8 @@ describe Projects::MilestonesController do
controller.instance_variable_set(:@project, project) controller.instance_variable_set(:@project, project)
end end
it_behaves_like 'milestone tabs'
describe "#show" do describe "#show" do
render_views render_views
......
...@@ -86,6 +86,9 @@ describe 'Milestone draggable', feature: true, js: true do ...@@ -86,6 +86,9 @@ describe 'Milestone draggable', feature: true, js: true do
visit namespace_project_milestone_path(project.namespace, project, milestone) visit namespace_project_milestone_path(project.namespace, project, milestone)
page.find("a[href='#tab-merge-requests']").click page.find("a[href='#tab-merge-requests']").click
wait_for_ajax
scroll_into_view('.milestone-content') scroll_into_view('.milestone-content')
drag_to(selector: '.merge_requests-sortable-list', list_to_index: 1) drag_to(selector: '.merge_requests-sortable-list', list_to_index: 1)
......
shared_examples 'milestone tabs' do
def go(path, extra_params = {})
params = if milestone.is_a?(GlobalMilestone)
{ group_id: group.id, id: milestone.safe_title, title: milestone.title }
else
{ namespace_id: project.namespace.to_param, project_id: project, id: milestone.iid }
end
get path, params.merge(extra_params)
end
describe '#merge_requests' do
context 'as html' do
before { go(:merge_requests, format: 'html') }
it 'redirects to milestone#show' do
expect(response).to redirect_to(milestone_path)
end
end
context 'as json' do
before { go(:merge_requests, format: 'json') }
it 'renders the merge requests tab template to a string' do
expect(response).to render_template('shared/milestones/_merge_requests_tab')
expect(json_response).to have_key('html')
end
end
end
describe '#participants' do
context 'as html' do
before { go(:participants, format: 'html') }
it 'redirects to milestone#show' do
expect(response).to redirect_to(milestone_path)
end
end
context 'as json' do
before { go(:participants, format: 'json') }
it 'renders the participants tab template to a string' do
expect(response).to render_template('shared/milestones/_participants_tab')
expect(json_response).to have_key('html')
end
end
end
describe '#labels' do
context 'as html' do
before { go(:labels, format: 'html') }
it 'redirects to milestone#show' do
expect(response).to redirect_to(milestone_path)
end
end
context 'as json' do
before { go(:labels, format: 'json') }
it 'renders the labels tab template to a string' do
expect(response).to render_template('shared/milestones/_labels_tab')
expect(json_response).to have_key('html')
end
end
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment