diff --git a/app/assets/javascripts/milestone.js.coffee b/app/assets/javascripts/milestone.js.coffee index d644d50b669..31f6c6d3d47 100644 --- a/app/assets/javascripts/milestone.js.coffee +++ b/app/assets/javascripts/milestone.js.coffee @@ -64,6 +64,7 @@ class @Milestone constructor: -> @bindIssuesSorting() @bindMergeRequestSorting() + @bindTabsSwitching bindIssuesSorting: -> $("#issues-list-unassigned, #issues-list-ongoing, #issues-list-closed").sortable( @@ -122,3 +123,12 @@ class @Milestone Milestone.updateMergeRequest(ui.item, merge_request_url, data) ).disableSelection() + + bindMergeRequestSorting: -> + $('a[data-toggle="tab"]').on 'show.bs.tab', (e) -> + currentTabClass = $(e.target).data('show') + previousTabClass = $(e.relatedTarget).data('show') + + $(previousTabClass).hide() + $(currentTabClass).removeClass('hidden') + $(currentTabClass).show() diff --git a/app/assets/stylesheets/pages/milestone.scss b/app/assets/stylesheets/pages/milestone.scss index e80dc9e84a1..9144a83647d 100644 --- a/app/assets/stylesheets/pages/milestone.scss +++ b/app/assets/stylesheets/pages/milestone.scss @@ -11,3 +11,60 @@ li.milestone { height: 6px; } } + +.milestone-content { + .issues-count { + margin-right: 17px; + float: right; + width: 105px; + } + + .issue-row { + .color-label { + border-radius: 2px; + padding: 3px !important; + } + + // Issue title + span a { + color: rgba(0,0,0,0.64); + } + } +} + +.milestone-summary { + margin-bottom: 25px; + + .milestone-stat { + margin-right: 10px; + } + + .time-elapsed { + color: $orange-light; + } +} + +.issues-sortable-list { + .issue-detail { + display: block; + + .issue-number{ + color: rgba(0,0,0,0.44); + margin-right: 5px; + } + .color-label { + padding: 6px 10px; + margin-right: 7px; + margin-top: 10px; + } + + .avatar { + float: none; + } + } +} + +.milestone-detail { + border-bottom: 1px solid $border-color; + padding: 20px 0; +} diff --git a/app/controllers/projects/milestones_controller.rb b/app/controllers/projects/milestones_controller.rb index a5c4ef1c7c7..21f30f278c8 100644 --- a/app/controllers/projects/milestones_controller.rb +++ b/app/controllers/projects/milestones_controller.rb @@ -35,6 +35,7 @@ class Projects::MilestonesController < Projects::ApplicationController @issues = @milestone.issues @users = @milestone.participants.uniq @merge_requests = @milestone.merge_requests + @labels = @milestone.labels end def create diff --git a/app/models/label.rb b/app/models/label.rb index 220da10a6ab..f93ce7e1c89 100644 --- a/app/models/label.rb +++ b/app/models/label.rb @@ -85,6 +85,10 @@ class Label < ActiveRecord::Base issues.opened.count end + def closed_issues_count + issues.closed.count + end + def template? template end diff --git a/app/models/milestone.rb b/app/models/milestone.rb index 9c4476c768e..cbe65d70997 100644 --- a/app/models/milestone.rb +++ b/app/models/milestone.rb @@ -27,6 +27,7 @@ class Milestone < ActiveRecord::Base belongs_to :project has_many :issues + has_many :labels, through: :issues has_many :merge_requests has_many :participants, through: :issues, source: :assignee @@ -109,6 +110,19 @@ class Milestone < ActiveRecord::Base 0 end + # Returns the elapsed time (in percent) since the Milestone creation date until today. + # If the Milestone doesn't have a due_date then returns 0 since we can't calculate the elapsed time. + # If the Milestone is overdue then it returns 100%. + def percent_time_used + return 0 unless due_date + return 100 if expired? + + duration = ((created_at - due_date.to_datetime) / 1.day) + days_elapsed = ((created_at - Time.now) / 1.day) + + ((days_elapsed.to_f / duration) * 100).floor + end + def expires_at if due_date if due_date.past? diff --git a/app/views/projects/milestones/_issue.html.haml b/app/views/projects/milestones/_issue.html.haml index 133d802aaca..ca51b8c745d 100644 --- a/app/views/projects/milestones/_issue.html.haml +++ b/app/views/projects/milestones/_issue.html.haml @@ -1,9 +1,10 @@ %li{ id: dom_id(issue, 'sortable'), class: 'issue-row', 'data-iid' => issue.iid, 'data-url' => issue_path(issue) } - .pull-right.assignee-icon - - if issue.assignee - = image_tag avatar_icon(issue.assignee, 16), class: "avatar s16", alt: '' %span - = link_to [@project.namespace.becomes(Namespace), @project, issue] do - %span.cgray ##{issue.iid} = link_to_gfm issue.title, [@project.namespace.becomes(Namespace), @project, issue], title: issue.title - + .issue-detail + = link_to [@project.namespace.becomes(Namespace), @project, issue] do + %span.issue-number ##{issue.iid} + - issue.labels.each do |label| + = render_colored_label(label) + - if issue.assignee + = image_tag avatar_icon(issue.assignee, 16), class: "avatar s24", alt: '' diff --git a/app/views/projects/milestones/_issues.html.haml b/app/views/projects/milestones/_issues.html.haml index 6e4df75a3df..6f8a341e478 100644 --- a/app/views/projects/milestones/_issues.html.haml +++ b/app/views/projects/milestones/_issues.html.haml @@ -1,6 +1,7 @@ .panel.panel-default - .panel-heading= title + .panel-heading + = title + .pull-right= issues.size %ul{ class: "well-list issues-sortable-list", id: "issues-list-#{id}", "data-state" => id } - issues.sort_by(&:position).each do |issue| = render 'issue', issue: issue - %li.light.ui-sort-disabled Drag and drop available diff --git a/app/views/projects/milestones/_merge_requests.html.haml b/app/views/projects/milestones/_merge_requests.html.haml index 00889a5eb24..9a5a02af215 100644 --- a/app/views/projects/milestones/_merge_requests.html.haml +++ b/app/views/projects/milestones/_merge_requests.html.haml @@ -3,4 +3,3 @@ %ul{ class: "well-list merge_requests-sortable-list", id: "merge_requests-list-#{id}", "data-state" => id } - merge_requests.sort_by(&:position).each do |merge_request| = render 'merge_request', merge_request: merge_request - %li.light.ui-sort-disabled Drag and drop available diff --git a/app/views/projects/milestones/show.html.haml b/app/views/projects/milestones/show.html.haml index 528a4f9552f..631bc8c3e9d 100644 --- a/app/views/projects/milestones/show.html.haml +++ b/app/views/projects/milestones/show.html.haml @@ -24,7 +24,7 @@ - else = link_to 'Reopen Milestone', namespace_project_milestone_path(@project.namespace, @project, @milestone, milestone: {state_event: :activate }), method: :put, class: "btn btn-reopen btn-nr btn-grouped" - = link_to namespace_project_milestone_path(@project.namespace, @project, @milestone), data: { confirm: 'Are you sure?' }, method: :delete, class: "btn btn-grouped btn-nr btn-remove" do + = link_to namespace_project_milestone_path(@project.namespace, @project, @milestone), data: { confirm: 'Are you sure?' }, method: :delete, class: "btn btn-grouped btn-nr" do = icon('trash-o') Delete @@ -32,7 +32,7 @@ = icon('pencil-square-o') Edit -.detail-page-description.content-block +.detail-page-description.milestone-detail.second-block %h2.title = markdown escape_once(@milestone.title), pipeline: :single_line %div @@ -47,44 +47,55 @@ %span All issues for this milestone are closed. You may close milestone now. .context.prepend-top-default - %p.lead - Progress: - #{@milestone.closed_items_count} closed - – - #{@milestone.open_items_count} open -   - %span.light #{@milestone.percent_complete}% complete - %span.pull-right= @milestone.expires_at + .milestone-summary + %h4 Progress + %strong= @milestone.issues.count + issues: + %span.milestone-stat + %strong= @milestone.open_items_count + open and + %strong= @milestone.closed_items_count + closed + %span.milestone-stat + %strong== #{@milestone.percent_complete}% + complete + %span.milestone-stat + %span.time-elapsed + %strong== #{@milestone.percent_time_used}% + time elapsed + %span.pull-right.tab-issues-buttons + - if can?(current_user, :create_issue, @project) + = link_to new_namespace_project_issue_path(@project.namespace, @project, issue: { milestone_id: @milestone.id }), class: "btn btn-grouped", title: "New Issue" do + %i.fa.fa-plus + New Issue + - if can?(current_user, :read_issue, @project) + = link_to 'Browse Issues', namespace_project_issues_path(@milestone.project.namespace, @milestone.project, milestone_title: @milestone.title), class: "btn btn-grouped" + %span.pull-right.tab-merge-requests-buttons.hidden + - if can?(current_user, :read_merge_request, @project) + = link_to 'Browse Merge Requests', namespace_project_merge_requests_path(@milestone.project.namespace, @milestone.project, milestone_title: @milestone.title), class: "btn btn-grouped" + = milestone_progress_bar(@milestone) %ul.nav-links.no-top.no-bottom %li.active - = link_to '#tab-issues', 'data-toggle' => 'tab' do + = link_to '#tab-issues', 'data-toggle' => 'tab', 'data-show' => '.tab-issues-buttons' do Issues %span.badge= @issues.count %li - = link_to '#tab-merge-requests', 'data-toggle' => 'tab' do + = link_to '#tab-merge-requests', 'data-toggle' => 'tab', 'data-show' => '.tab-merge-requests-buttons' do Merge Requests %span.badge= @merge_requests.count %li = link_to '#tab-participants', 'data-toggle' => 'tab' do Participants %span.badge= @users.count + %li + = link_to '#tab-labels', 'data-toggle' => 'tab', 'data-show' => '.tab-issues-buttons' do + Labels + %span.badge= @labels.count -.tab-content +.tab-content.milestone-content .tab-pane.active#tab-issues - .content-block.oneline-block - .controls - - if can?(current_user, :create_issue, @project) - = link_to new_namespace_project_issue_path(@project.namespace, @project, issue: { milestone_id: @milestone.id }), class: "btn btn-grouped", title: "New Issue" do - %i.fa.fa-plus - New Issue - - if can?(current_user, :read_issue, @project) - = link_to 'Browse Issues', namespace_project_issues_path(@milestone.project.namespace, @milestone.project, milestone_title: @milestone.title), class: "btn btn-grouped" - - .oneline - All issues in this milestone - .row.prepend-top-default .col-md-4 = render('issues', title: 'Unstarted Issues (open and unassigned)', issues: @issues.opened.unassigned, id: 'unassigned') @@ -94,14 +105,6 @@ = render('issues', title: 'Completed Issues (closed)', issues: @issues.closed, id: 'closed') .tab-pane#tab-merge-requests - .content-block.oneline-block - .controls - - if can?(current_user, :read_merge_request, @project) - = link_to 'Browse Merge Requests', namespace_project_merge_requests_path(@milestone.project.namespace, @milestone.project, milestone_title: @milestone.title), class: "btn btn-grouped" - - .oneline - All merge requests in this milestone - .row.prepend-top-default .col-md-3 = render('merge_requests', title: 'Work in progress (open and unassigned)', merge_requests: @merge_requests.opened.unassigned, id: 'unassigned') @@ -117,9 +120,6 @@ = render 'merge_request', merge_request: merge_request .tab-pane#tab-participants - .content-block.oneline-block - All participants to this milestone - %ul.bordered-list - @users.each do |user| %li @@ -128,3 +128,18 @@ %strong= truncate(user.name, lenght: 40) %br %small.cgray= user.username + + .tab-pane#tab-labels + %ul.bordered-list.manage-labels-list + - @labels.each do |label| + %li + = render_colored_label(label) + - args = [@milestone.project.namespace, @milestone.project, milestone_title: @milestone.title, label_name: label.title] + - options = args.extract_options! + + %span.issues-count + = link_to namespace_project_issues_path(*args, options.merge(state: 'opened')) do + = pluralize label.open_issues_count, 'open issue' + %span.issues-count + = link_to namespace_project_issues_path(*args, options.merge(state: 'closed')) do + = pluralize label.closed_issues_count, 'closed issue' diff --git a/features/project/milestone.feature b/features/project/milestone.feature new file mode 100644 index 00000000000..e0f4c0e9d7c --- /dev/null +++ b/features/project/milestone.feature @@ -0,0 +1,23 @@ +Feature: Project Milestone + Background: + Given I sign in as a user + And I own project "Shop" + And project "Shop" has labels: "bug", "feature", "enhancement" + And project "Shop" has milestone "v2.2" + And milestone has issue "Bugfix1" with labels: "bug", "feature" + 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" + + @javascript + Scenario: Listing labels from labels tab + Given I visit project "Shop" milestones page + And I click link "v2.2" + And I click link "Labels" + Then I should see the list of labels + And I should see the labels "bug", "enhancement" and "feature" diff --git a/features/steps/project/project_milestone.rb b/features/steps/project/project_milestone.rb new file mode 100644 index 00000000000..ec881c0d8fc --- /dev/null +++ b/features/steps/project/project_milestone.rb @@ -0,0 +1,53 @@ +class Spinach::Features::ProjectMilestone < Spinach::FeatureSteps + include SharedAuthentication + include SharedProject + include SharedPaths + + step 'milestone has issue "Bugfix1" with labels: "bug", "feature"' do + project = Project.find_by(name: "Shop") + milestone = project.milestones.find_by(title: 'v2.2') + issue = create(:issue, title: "Bugfix1", project: project, milestone: milestone) + issue.labels << project.labels.find_by(title: 'bug') + issue.labels << project.labels.find_by(title: 'feature') + end + + step 'milestone has issue "Bugfix2" with labels: "bug", "enhancement"' do + project = Project.find_by(name: "Shop") + milestone = project.milestones.find_by(title: 'v2.2') + issue = create(:issue, title: "Bugfix2", project: project, milestone: milestone) + issue.labels << project.labels.find_by(title: 'bug') + issue.labels << project.labels.find_by(title: 'enhancement') + end + + step 'project "Shop" has milestone "v2.2"' do + project = Project.find_by(name: "Shop") + milestone = create(:milestone, + title: "v2.2", + project: project, + description: "# Description header" + ) + 3.times { create(:issue, project: project, milestone: milestone) } + end + + step 'I should see the list of labels' do + expect(page).to have_selector('ul.manage-labels-list') + end + + step 'I should see the labels "bug", "enhancement" and "feature"' do + page.within('#tab-issues') do + expect(page).to have_content 'bug' + expect(page).to have_content 'enhancement' + expect(page).to have_content 'feature' + end + end + + step 'I click link "v2.2"' do + click_link "v2.2" + end + + step 'I click link "Labels"' do + page.within('.nav-links') do + page.find(:xpath, "//a[@href='#tab-labels']").click + end + end +end