diff options
author | Sean McGivern <sean@mcgivern.me.uk> | 2017-07-07 15:40:28 +0000 |
---|---|---|
committer | Sean McGivern <sean@mcgivern.me.uk> | 2017-07-07 15:40:28 +0000 |
commit | 8b33e654c09aaa20545e7c246585fa2d3217cecb (patch) | |
tree | 9cd6fc560ea034f41ae7607e77a10561e67a9213 /app | |
parent | 9fc63aa7f65ef74eb40ebc884de8fc8e031969db (diff) | |
parent | 1dab640357fa1ba8992757499e4167fcd4ce6276 (diff) | |
download | gitlab-ce-8b33e654c09aaa20545e7c246585fa2d3217cecb.tar.gz |
Merge branch 'master' into '33929-allow-to-enable-perf-bar-for-a-group'
# Conflicts:
# db/schema.rb
Diffstat (limited to 'app')
63 files changed, 730 insertions, 278 deletions
diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js index 8f716696605..ae19592ecbe 100644 --- a/app/assets/javascripts/dispatcher.js +++ b/app/assets/javascripts/dispatcher.js @@ -322,7 +322,7 @@ import PerformanceBar from './performance_bar'; new gl.Members(); new UsersSelect(); break; - case 'projects:settings:members:show': + case 'projects:project_members:index': new gl.MemberExpirationDate('.js-access-expiration-date-groups'); new GroupsSelect(); new gl.MemberExpirationDate(); diff --git a/app/assets/javascripts/pipeline_schedules/pipeline_schedule_form_bundle.js b/app/assets/javascripts/pipeline_schedules/pipeline_schedule_form_bundle.js index b424e7f205d..50c725aa3d5 100644 --- a/app/assets/javascripts/pipeline_schedules/pipeline_schedule_form_bundle.js +++ b/app/assets/javascripts/pipeline_schedules/pipeline_schedule_form_bundle.js @@ -3,6 +3,7 @@ import Translate from '../vue_shared/translate'; import intervalPatternInput from './components/interval_pattern_input.vue'; import TimezoneDropdown from './components/timezone_dropdown'; import TargetBranchDropdown from './components/target_branch_dropdown'; +import { setupPipelineVariableList } from './setup_pipeline_variable_list'; Vue.use(Translate); @@ -39,4 +40,6 @@ document.addEventListener('DOMContentLoaded', () => { gl.timezoneDropdown = new TimezoneDropdown(); gl.targetBranchDropdown = new TargetBranchDropdown(); gl.pipelineScheduleFieldErrors = new gl.GlFieldErrors(formElement); + + setupPipelineVariableList($('.js-pipeline-variable-list')); }); diff --git a/app/assets/javascripts/pipeline_schedules/setup_pipeline_variable_list.js b/app/assets/javascripts/pipeline_schedules/setup_pipeline_variable_list.js new file mode 100644 index 00000000000..644efd10509 --- /dev/null +++ b/app/assets/javascripts/pipeline_schedules/setup_pipeline_variable_list.js @@ -0,0 +1,71 @@ +function insertRow($row) { + const $rowClone = $row.clone(); + $rowClone.removeAttr('data-is-persisted'); + $rowClone.find('input, textarea').val(''); + $row.after($rowClone); +} + +function removeRow($row) { + const isPersisted = gl.utils.convertPermissionToBoolean($row.attr('data-is-persisted')); + + if (isPersisted) { + $row.hide(); + $row + .find('.js-destroy-input') + .val(1); + } else { + $row.remove(); + } +} + +function checkIfRowTouched($row) { + return $row.find('.js-user-input').toArray().some(el => $(el).val().length > 0); +} + +function setupPipelineVariableList(parent = document) { + const $parent = $(parent); + + $parent.on('click', '.js-row-remove-button', (e) => { + const $row = $(e.currentTarget).closest('.js-row'); + removeRow($row); + + e.preventDefault(); + }); + + // Remove any empty rows except the last r + $parent.on('blur', '.js-user-input', (e) => { + const $row = $(e.currentTarget).closest('.js-row'); + + const isTouched = checkIfRowTouched($row); + if ($row.is(':not(:last-child)') && !isTouched) { + removeRow($row); + } + }); + + // Always make sure there is an empty last row + $parent.on('input', '.js-user-input', () => { + const $lastRow = $parent.find('.js-row').last(); + + const isTouched = checkIfRowTouched($lastRow); + if (isTouched) { + insertRow($lastRow); + } + }); + + // Clear out the empty last row so it + // doesn't get submitted and throw validation errors + $parent.closest('form').on('submit', () => { + const $lastRow = $parent.find('.js-row').last(); + + const isTouched = checkIfRowTouched($lastRow); + if (!isTouched) { + $lastRow.find('input, textarea').attr('name', ''); + } + }); +} + +export { + setupPipelineVariableList, + insertRow, + removeRow, +}; diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index 2e4b1280a97..8b4ea0fbff2 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -153,6 +153,7 @@ $code_line_height: 1.6; * Padding */ $gl-padding: 16px; +$gl-col-padding: 15px; $gl-btn-padding: 10px; $gl-input-padding: 10px; $gl-vert-padding: 6px; @@ -443,6 +444,7 @@ $logs-p-color: #333; /* * Forms */ +$input-height: 34px; $input-danger-bg: #f2dede; $input-danger-border: $red-400; $input-group-addon-bg: #f7f8fa; @@ -575,6 +577,12 @@ $stage-hover-border: #d1e7fc; $action-icon-color: #d6d6d6; /* +Pipeline Schedules +*/ +$pipeline-variable-remove-button-width: calc(1em + #{2 * $gl-padding}); + + +/* Filtered Search */ $filter-name-resting-color: #f8f8f8; diff --git a/app/assets/stylesheets/pages/pipeline_schedules.scss b/app/assets/stylesheets/pages/pipeline_schedules.scss index 595eb40fec7..dc719a6ba94 100644 --- a/app/assets/stylesheets/pages/pipeline_schedules.scss +++ b/app/assets/stylesheets/pages/pipeline_schedules.scss @@ -74,3 +74,84 @@ margin-right: 3px; } } + +.pipeline-variable-list { + margin-left: 0; + margin-bottom: 0; + padding-left: 0; + list-style: none; + clear: both; +} + +.pipeline-variable-row { + display: flex; + align-items: flex-end; + + &:not(:last-child) { + margin-bottom: $gl-btn-padding; + } + + @media (max-width: $screen-sm-max) { + padding-right: $gl-col-padding; + } + + &:last-child { + & .pipeline-variable-row-remove-button { + display: none; + } + + @media (max-width: $screen-sm-max) { + & .pipeline-variable-value-input { + margin-right: $pipeline-variable-remove-button-width; + } + } + + @media (max-width: $screen-xs-max) { + .pipeline-variable-row-body { + margin-right: $pipeline-variable-remove-button-width; + } + } + } +} + +.pipeline-variable-row-body { + display: flex; + width: calc(75% - #{$gl-col-padding}); + padding-left: $gl-col-padding; + + @media (max-width: $screen-sm-max) { + width: 100%; + } + + @media (max-width: $screen-xs-max) { + display: block; + } +} + +.pipeline-variable-key-input { + margin-right: $gl-btn-padding; + + @media (max-width: $screen-xs-max) { + margin-bottom: $gl-btn-padding; + } +} + +.pipeline-variable-row-remove-button { + flex-shrink: 0; + display: flex; + justify-content: center; + align-items: center; + width: $pipeline-variable-remove-button-width; + height: $input-height; + padding: 0; + background: transparent; + border: 0; + color: $gl-text-color-secondary; + @include transition(color); + + &:hover, + &:focus { + outline: none; + color: $gl-text-color; + } +} diff --git a/app/controllers/concerns/membership_actions.rb b/app/controllers/concerns/membership_actions.rb index 47d9ae350ae..c6b1e443de6 100644 --- a/app/controllers/concerns/membership_actions.rb +++ b/app/controllers/concerns/membership_actions.rb @@ -70,7 +70,7 @@ module MembershipActions def members_page_url if membershipable.is_a?(Project) - project_settings_members_path(membershipable) + project_project_members_path(membershipable) else polymorphic_url([membershipable, :members]) end diff --git a/app/controllers/dashboard/labels_controller.rb b/app/controllers/dashboard/labels_controller.rb index dd1d46a68c7..9dcb3a0eb6d 100644 --- a/app/controllers/dashboard/labels_controller.rb +++ b/app/controllers/dashboard/labels_controller.rb @@ -1,9 +1,14 @@ class Dashboard::LabelsController < Dashboard::ApplicationController def index - labels = LabelsFinder.new(current_user).execute - respond_to do |format| format.json { render json: LabelSerializer.new.represent_appearance(labels) } end end + + def labels + finder_params = { project_ids: projects.select(:id) } + labels = LabelsFinder.new(current_user, finder_params).execute + + GlobalLabel.build_collection(labels) + end end diff --git a/app/controllers/groups/milestones_controller.rb b/app/controllers/groups/milestones_controller.rb index 6b1d418fc9a..5c10d7bc261 100644 --- a/app/controllers/groups/milestones_controller.rb +++ b/app/controllers/groups/milestones_controller.rb @@ -2,13 +2,13 @@ class Groups::MilestonesController < Groups::ApplicationController include MilestoneActions before_action :group_projects - before_action :milestone, only: [:show, :update, :merge_requests, :participants, :labels] - before_action :authorize_admin_milestones!, only: [:new, :create, :update] + before_action :milestone, only: [:edit, :show, :update, :merge_requests, :participants, :labels] + before_action :authorize_admin_milestones!, only: [:edit, :new, :create, :update] def index respond_to do |format| format.html do - @milestone_states = GlobalMilestone.states_count(@projects) + @milestone_states = GlobalMilestone.states_count(group_projects, group) @milestones = Kaminari.paginate_array(milestones).page(params[:page]) end format.json do @@ -22,49 +22,41 @@ class Groups::MilestonesController < Groups::ApplicationController end def create - project_ids = params[:milestone][:project_ids].reject(&:blank?) - title = milestone_params[:title] + @milestone = Milestones::CreateService.new(group, current_user, milestone_params).execute - if create_milestones(project_ids) - redirect_to milestone_path(title) + if @milestone.persisted? + redirect_to milestone_path else - render_new_with_error(project_ids.empty?) + render "new" end end def show end - def update - @milestone.milestones.each do |milestone| - Milestones::UpdateService.new(milestone.project, current_user, milestone_params).execute(milestone) - end - - redirect_back_or_default(default: milestone_path(@milestone.title)) + def edit + render_404 if @milestone.is_legacy_group_milestone? end - private - - def create_milestones(project_ids) - return false unless project_ids.present? + def update + # Keep this compatible with legacy group milestones where we have to update + # all projects milestones states at once. + if @milestone.is_legacy_group_milestone? + update_params = milestone_params.select { |key| key == "state_event" } + milestones = @milestone.milestones + else + update_params = milestone_params + milestones = [@milestone] + end - ActiveRecord::Base.transaction do - @projects.where(id: project_ids).each do |project| - Milestones::CreateService.new(project, current_user, milestone_params).execute - end + milestones.each do |milestone| + Milestones::UpdateService.new(milestone.parent, current_user, update_params).execute(milestone) end - true - rescue ActiveRecord::ActiveRecordError => e - flash.now[:alert] = "An error occurred while creating the milestone: #{e.message}" - false + redirect_to milestone_path end - def render_new_with_error(empty_project_ids) - @milestone = Milestone.new(milestone_params) - @milestone.errors.add(:base, "Please select at least one project.") if empty_project_ids - render :new - end + private def authorize_admin_milestones! return render_404 unless can?(current_user, :admin_milestones, group) @@ -74,16 +66,31 @@ class Groups::MilestonesController < Groups::ApplicationController params.require(:milestone).permit(:title, :description, :start_date, :due_date, :state_event) end - def milestone_path(title) - group_milestone_path(@group, title.to_slug.to_s, title: title) + def milestone_path + if @milestone.is_legacy_group_milestone? + group_milestone_path(group, @milestone.safe_title, title: @milestone.title) + else + group_milestone_path(group, @milestone.iid) + end end def milestones - @milestones = GroupMilestone.build_collection(@group, @projects, params) + search_params = params.merge(group_ids: group.id) + + milestones = MilestonesFinder.new(search_params).execute + legacy_milestones = GroupMilestone.build_collection(group, group_projects, params) + + milestones + legacy_milestones end def milestone - @milestone = GroupMilestone.build(@group, @projects, params[:title]) + @milestone = + if params[:title] + GroupMilestone.build(group, group_projects, params[:title]) + else + group.milestones.find_by_iid(params[:id]) + end + render_404 unless @milestone end end diff --git a/app/controllers/projects/group_links_controller.rb b/app/controllers/projects/group_links_controller.rb index 8fc614b414d..f59200d3b1f 100644 --- a/app/controllers/projects/group_links_controller.rb +++ b/app/controllers/projects/group_links_controller.rb @@ -22,7 +22,7 @@ class Projects::GroupLinksController < Projects::ApplicationController flash[:alert] = 'Please select a group.' end - redirect_to project_settings_members_path(project) + redirect_to project_project_members_path(project) end def update @@ -36,7 +36,7 @@ class Projects::GroupLinksController < Projects::ApplicationController respond_to do |format| format.html do - redirect_to project_settings_members_path(project), status: 302 + redirect_to project_project_members_path(project), status: 302 end format.js { head :ok } end diff --git a/app/controllers/projects/milestones_controller.rb b/app/controllers/projects/milestones_controller.rb index a80562e77ce..c94384d2a1a 100644 --- a/app/controllers/projects/milestones_controller.rb +++ b/app/controllers/projects/milestones_controller.rb @@ -13,20 +13,16 @@ class Projects::MilestonesController < Projects::ApplicationController respond_to :html def index - @milestones = - case params[:state] - when 'all' then @project.milestones - when 'closed' then @project.milestones.closed - else @project.milestones.active - end - @sort = params[:sort] || 'due_date_asc' - @milestones = @milestones.sort(@sort) + @milestones = milestones.sort(@sort) respond_to do |format| format.html do @project_namespace = @project.namespace.becomes(Namespace) - @milestones = @milestones.includes(:project) + # We need to show group milestones in the JSON response + # so that people can filter by and assign group milestones, + # but we don't need to show them on the project milestones page itself. + @milestones = @milestones.for_projects @milestones = @milestones.page(params[:page]) end format.json do @@ -45,12 +41,13 @@ class Projects::MilestonesController < Projects::ApplicationController end def show + @project_namespace = @project.namespace.becomes(Namespace) end def create @milestone = Milestones::CreateService.new(project, current_user, milestone_params).execute - if @milestone.save + if @milestone.valid? redirect_to project_milestone_path(@project, @milestone) else render "new" @@ -85,6 +82,18 @@ class Projects::MilestonesController < Projects::ApplicationController protected + def milestones + @milestones ||= begin + if @project.group && can?(current_user, :read_group, @project.group) + group = @project.group + end + + search_params = params.merge(project_ids: @project.id, group_ids: group&.id) + + MilestonesFinder.new(search_params).execute + end + end + def milestone @milestone ||= @project.milestones.find_by!(iid: params[:id]) end diff --git a/app/controllers/projects/pipeline_schedules_controller.rb b/app/controllers/projects/pipeline_schedules_controller.rb index 0d967a7e691..ec7c645df5a 100644 --- a/app/controllers/projects/pipeline_schedules_controller.rb +++ b/app/controllers/projects/pipeline_schedules_controller.rb @@ -1,11 +1,11 @@ class Projects::PipelineSchedulesController < Projects::ApplicationController + before_action :schedule, except: [:index, :new, :create] + before_action :authorize_read_pipeline_schedule! before_action :authorize_create_pipeline_schedule!, only: [:new, :create] - before_action :authorize_update_pipeline_schedule!, only: [:edit, :take_ownership, :update] + before_action :authorize_update_pipeline_schedule!, except: [:index, :new, :create] before_action :authorize_admin_pipeline_schedule!, only: [:destroy] - before_action :schedule, only: [:edit, :update, :destroy, :take_ownership] - def index @scope = params[:scope] @all_schedules = PipelineSchedulesFinder.new(@project).execute @@ -53,7 +53,7 @@ class Projects::PipelineSchedulesController < Projects::ApplicationController redirect_to pipeline_schedules_path(@project), status: 302 else redirect_to pipeline_schedules_path(@project), - status: 302, + status: :forbidden, alert: _("Failed to remove the pipeline schedule") end end @@ -66,6 +66,15 @@ class Projects::PipelineSchedulesController < Projects::ApplicationController def schedule_params params.require(:schedule) - .permit(:description, :cron, :cron_timezone, :ref, :active) + .permit(:description, :cron, :cron_timezone, :ref, :active, + variables_attributes: [:id, :key, :value, :_destroy] ) + end + + def authorize_update_pipeline_schedule! + return access_denied! unless can?(current_user, :update_pipeline_schedule, schedule) + end + + def authorize_admin_pipeline_schedule! + return access_denied! unless can?(current_user, :admin_pipeline_schedule, schedule) end end diff --git a/app/controllers/projects/project_members_controller.rb b/app/controllers/projects/project_members_controller.rb index 57a6686f66c..f8ff7413b53 100644 --- a/app/controllers/projects/project_members_controller.rb +++ b/app/controllers/projects/project_members_controller.rb @@ -6,8 +6,23 @@ class Projects::ProjectMembersController < Projects::ApplicationController before_action :authorize_admin_project_member!, except: [:index, :leave, :request_access] def index - sort = params[:sort].presence || sort_value_name - redirect_to project_settings_members_path(@project, sort: sort) + @sort = params[:sort].presence || sort_value_name + @group_links = @project.project_group_links + + @skip_groups = @group_links.pluck(:group_id) + @skip_groups << @project.namespace_id unless @project.personal? + @skip_groups += @project.group.ancestors.pluck(:id) if @project.group + + @project_members = MembersFinder.new(@project, current_user).execute + + if params[:search].present? + @project_members = @project_members.joins(:user).merge(User.search(params[:search])) + @group_links = @group_links.where(group_id: @project.invited_groups.search(params[:search]).select(:id)) + end + + @project_members = @project_members.sort(@sort).page(params[:page]) + @requesters = AccessRequestsFinder.new(@project).execute(current_user) + @project_member = @project.project_members.new end def update @@ -19,7 +34,7 @@ class Projects::ProjectMembersController < Projects::ApplicationController end def resend_invite - redirect_path = project_settings_members_path(@project) + redirect_path = project_project_members_path(@project) @project_member = @project.project_members.find(params[:id]) @@ -42,7 +57,7 @@ class Projects::ProjectMembersController < Projects::ApplicationController return render_404 end - redirect_to(project_settings_members_path(project), + redirect_to(project_project_members_path(project), notice: notice) end diff --git a/app/controllers/projects/settings/members_controller.rb b/app/controllers/projects/settings/members_controller.rb deleted file mode 100644 index 54f9dceddef..00000000000 --- a/app/controllers/projects/settings/members_controller.rb +++ /dev/null @@ -1,27 +0,0 @@ -module Projects - module Settings - class MembersController < Projects::ApplicationController - include SortingHelper - - def show - @sort = params[:sort].presence || sort_value_name - @group_links = @project.project_group_links - - @skip_groups = @group_links.pluck(:group_id) - @skip_groups << @project.namespace_id unless @project.personal? - @skip_groups += @project.group.ancestors.pluck(:id) if @project.group - - @project_members = MembersFinder.new(@project, current_user).execute - - if params[:search].present? - @project_members = @project_members.joins(:user).merge(User.search(params[:search])) - @group_links = @group_links.where(group_id: @project.invited_groups.search(params[:search]).select(:id)) - end - - @project_members = @project_members.sort(@sort).page(params[:page]) - @requesters = AccessRequestsFinder.new(@project).execute(current_user) - @project_member = @project.project_members.new - end - end - end -end diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb index 7bc2117f61e..d81e9ed17d4 100644 --- a/app/finders/issuable_finder.rb +++ b/app/finders/issuable_finder.rb @@ -147,9 +147,17 @@ class IssuableFinder @milestones = if milestones? - scope = Milestone.where(project_id: projects) + if project? + group_id = project.group&.id + project_id = project.id + end + + group_id = group.id if group - scope.where(title: params[:milestone_title]) + search_params = + { title: params[:milestone_title], project_ids: project_id, group_ids: group_id } + + MilestonesFinder.new(search_params).execute else Milestone.none end @@ -331,11 +339,6 @@ class IssuableFinder items = items.left_joins_milestones.where('milestones.start_date <= NOW()') else items = items.with_milestone(params[:milestone_title]) - items_projects = projects(items) - - if items_projects - items = items.where(milestones: { project_id: items_projects }) - end end end diff --git a/app/finders/milestones_finder.rb b/app/finders/milestones_finder.rb index 630c73c2a94..23c42a5f662 100644 --- a/app/finders/milestones_finder.rb +++ b/app/finders/milestones_finder.rb @@ -1,12 +1,55 @@ +# Search for milestones +# +# params - Hash +# project_ids: Array of project ids or single project id. +# group_ids: Array of group ids or single group id. +# order - Orders by field default due date asc. +# title - filter by title. +# state - filters by state. + class MilestonesFinder - def execute(projects, params) - milestones = Milestone.of_projects(projects) - milestones = milestones.reorder("due_date ASC") - - case params[:state] - when 'closed' then milestones.closed - when 'all' then milestones - else milestones.active + attr_reader :params, :project_ids, :group_ids + + def initialize(params = {}) + @project_ids = Array(params[:project_ids]) + @group_ids = Array(params[:group_ids]) + @params = params + end + + def execute + return Milestone.none if project_ids.empty? && group_ids.empty? + + items = Milestone.all + items = by_groups_and_projects(items) + items = by_title(items) + items = by_state(items) + + order(items) + end + + private + + def by_groups_and_projects(items) + items.for_projects_and_groups(project_ids, group_ids) + end + + def by_title(items) + if params[:title] + items.where(title: params[:title]) + else + items + end + end + + def by_state(items) + Milestone.filter_by_state(items, params[:state]) + end + + def order(items) + if params.has_key?(:order) + items.reorder(params[:order]) + else + items.reorder('due_date ASC') end end end diff --git a/app/helpers/gitlab_routing_helper.rb b/app/helpers/gitlab_routing_helper.rb index b5f4bbe97dc..2e36d0fdb5a 100644 --- a/app/helpers/gitlab_routing_helper.rb +++ b/app/helpers/gitlab_routing_helper.rb @@ -97,7 +97,7 @@ module GitlabRoutingHelper ## Members def project_members_url(project, *args) - project_project_members_url(project) + project_project_members_url(project, *args) end def project_member_path(project_member, *args) diff --git a/app/helpers/milestones_helper.rb b/app/helpers/milestones_helper.rb index 8c7851dcfc2..f8860bfee99 100644 --- a/app/helpers/milestones_helper.rb +++ b/app/helpers/milestones_helper.rb @@ -54,8 +54,10 @@ module MilestonesHelper def milestone_class_for_state(param, check, match_blank_param = false) if match_blank_param 'active' if param.blank? || param == check + elsif param == check + 'active' else - 'active' if param == check + check end end @@ -147,4 +149,14 @@ module MilestonesHelper labels_dashboard_milestone_path(milestone, title: milestone.title, format: :json) end end + + def group_milestone_route(milestone, params = {}) + params = nil if params.empty? + + if milestone.is_legacy_group_milestone? + group_milestone_path(@group, milestone.safe_title, title: milestone.title, milestone: params) + else + group_milestone_path(@group, milestone.iid, milestone: params) + end + end end diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index 5022b291f7f..25969adb649 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -267,15 +267,15 @@ module ProjectsHelper def tab_ability_map { - environments: :read_environment, - milestones: :read_milestone, - snippets: :read_project_snippet, - settings: :admin_project, - builds: :read_build, - labels: :read_label, - issues: :read_issue, - team: :read_project_member, - wiki: :read_wiki + environments: :read_environment, + milestones: :read_milestone, + snippets: :read_project_snippet, + settings: :admin_project, + builds: :read_build, + labels: :read_label, + issues: :read_issue, + project_members: :read_project_member, + wiki: :read_wiki } end diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb index 8c44f4b0934..fd7ab59ce64 100644 --- a/app/helpers/search_helper.rb +++ b/app/helpers/search_helper.rb @@ -75,7 +75,7 @@ module SearchHelper { category: "Current Project", label: "Merge Requests", url: project_merge_requests_path(@project) }, { category: "Current Project", label: "Milestones", url: project_milestones_path(@project) }, { category: "Current Project", label: "Snippets", url: project_snippets_path(@project) }, - { category: "Current Project", label: "Members", url: project_settings_members_path(@project) }, + { category: "Current Project", label: "Members", url: project_project_members_path(@project) }, { category: "Current Project", label: "Wiki", url: project_wikis_path(@project) } ] else diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 25e75a19f37..432f3f242eb 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -203,6 +203,7 @@ module Ci variables += project.group.secret_variables_for(ref, project).map(&:to_runner_variable) if project.group variables += secret_variables(environment: environment) variables += trigger_request.user_variables if trigger_request + variables += pipeline.pipeline_schedule.job_variables if pipeline.pipeline_schedule variables += persisted_environment_variables if environment variables diff --git a/app/models/ci/pipeline_schedule.rb b/app/models/ci/pipeline_schedule.rb index 45d8cd34359..e4ae1b35f66 100644 --- a/app/models/ci/pipeline_schedule.rb +++ b/app/models/ci/pipeline_schedule.rb @@ -9,17 +9,21 @@ module Ci belongs_to :owner, class_name: 'User' has_one :last_pipeline, -> { order(id: :desc) }, class_name: 'Ci::Pipeline' has_many :pipelines + has_many :variables, class_name: 'Ci::PipelineScheduleVariable' validates :cron, unless: :importing?, cron: true, presence: { unless: :importing? } validates :cron_timezone, cron_timezone: true, presence: { unless: :importing? } validates :ref, presence: { unless: :importing? } validates :description, presence: true + validates :variables, variable_duplicates: true before_save :set_next_run_at scope :active, -> { where(active: true) } scope :inactive, -> { where(active: false) } + accepts_nested_attributes_for :variables, allow_destroy: true + def owned_by?(current_user) owner == current_user end @@ -56,5 +60,9 @@ module Ci Gitlab::Ci::CronParser.new(worker_cron, worker_time_zone) .next_time_from(next_run_at) end + + def job_variables + variables&.map(&:to_runner_variable) || [] + end end end diff --git a/app/models/ci/pipeline_schedule_variable.rb b/app/models/ci/pipeline_schedule_variable.rb new file mode 100644 index 00000000000..1ff177616e8 --- /dev/null +++ b/app/models/ci/pipeline_schedule_variable.rb @@ -0,0 +1,8 @@ +module Ci + class PipelineScheduleVariable < ActiveRecord::Base + extend Ci::Model + include HasVariable + + belongs_to :pipeline_schedule + end +end diff --git a/app/models/concerns/internal_id.rb b/app/models/concerns/internal_id.rb index 5382dde6765..67a0adfcd56 100644 --- a/app/models/concerns/internal_id.rb +++ b/app/models/concerns/internal_id.rb @@ -8,7 +8,8 @@ module InternalId def set_iid if iid.blank? - records = project.send(self.class.name.tableize) + parent = project || group + records = parent.send(self.class.name.tableize) records = records.with_deleted if self.paranoid? max_iid = records.maximum(:iid) diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index 23cb85600da..13fe9d09c69 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -30,6 +30,7 @@ module Issuable belongs_to :updated_by, class_name: "User" belongs_to :last_edited_by, class_name: 'User' belongs_to :milestone + has_many :notes, as: :noteable, inverse_of: :noteable, dependent: :destroy do # rubocop:disable Cop/ActiveRecordDependent def authors_loaded? # We check first if we're loaded to not load unnecessarily. diff --git a/app/models/concerns/milestoneish.rb b/app/models/concerns/milestoneish.rb index 01599ce49c6..f0998465822 100644 --- a/app/models/concerns/milestoneish.rb +++ b/app/models/concerns/milestoneish.rb @@ -70,6 +70,22 @@ module Milestoneish due_date && due_date.past? end + def is_group_milestone? + false + end + + def is_project_milestone? + false + end + + def is_legacy_group_milestone? + false + end + + def is_dashboard_milestone? + false + end + private def count_issues_by_state(user) diff --git a/app/models/dashboard_milestone.rb b/app/models/dashboard_milestone.rb index 646c1e5ce1a..fac7c5e5c85 100644 --- a/app/models/dashboard_milestone.rb +++ b/app/models/dashboard_milestone.rb @@ -2,4 +2,8 @@ class DashboardMilestone < GlobalMilestone def issues_finder_params { authorized_only: true } end + + def is_dashboard_milestone? + true + end end diff --git a/app/models/global_label.rb b/app/models/global_label.rb index 698a7bbd327..2a1b7564962 100644 --- a/app/models/global_label.rb +++ b/app/models/global_label.rb @@ -2,7 +2,7 @@ class GlobalLabel attr_accessor :title, :labels alias_attribute :name, :title - delegate :color, :description, to: :@first_label + delegate :color, :text_color, :description, to: :@first_label def for_display @first_label diff --git a/app/models/global_milestone.rb b/app/models/global_milestone.rb index 538615130a7..c0864769314 100644 --- a/app/models/global_milestone.rb +++ b/app/models/global_milestone.rb @@ -2,6 +2,7 @@ class GlobalMilestone include Milestoneish EPOCH = DateTime.parse('1970-01-01') + STATE_COUNT_HASH = { opened: 0, closed: 0, all: 0 }.freeze attr_accessor :title, :milestones alias_attribute :name, :title @@ -11,7 +12,10 @@ class GlobalMilestone end def self.build_collection(projects, params) - child_milestones = MilestonesFinder.new.execute(projects, params) + params = + { project_ids: projects.map(&:id), state: params[:state] } + + child_milestones = MilestonesFinder.new(params).execute milestones = child_milestones.select(:id, :title).group_by(&:title).map do |title, grouped| milestones_relation = Milestone.where(id: grouped.map(&:id)) @@ -28,13 +32,42 @@ class GlobalMilestone new(title, child_milestones) end - def self.states_count(projects) - relation = MilestonesFinder.new.execute(projects, state: 'all') - milestones_by_state_and_title = relation.reorder(nil).group(:state, :title).count + def self.states_count(projects, group = nil) + legacy_group_milestones_count = legacy_group_milestone_states_count(projects) + group_milestones_count = group_milestones_states_count(group) + + legacy_group_milestones_count.merge(group_milestones_count) do |k, legacy_group_milestones_count, group_milestones_count| + legacy_group_milestones_count + group_milestones_count + end + end + + def self.group_milestones_states_count(group) + return STATE_COUNT_HASH unless group + + params = { group_ids: [group.id], state: 'all', order: nil } + + relation = MilestonesFinder.new(params).execute + grouped_by_state = relation.group(:state).count + + { + opened: grouped_by_state['active'] || 0, + closed: grouped_by_state['closed'] || 0, + all: grouped_by_state.values.sum + } + end + + # Counts the legacy group milestones which must be grouped by title + def self.legacy_group_milestone_states_count(projects) + return STATE_COUNT_HASH unless projects + + params = { project_ids: projects.map(&:id), state: 'all', order: nil } + + relation = MilestonesFinder.new(params).execute + project_milestones_by_state_and_title = relation.group(:state, :title).count - opened = count_by_state(milestones_by_state_and_title, 'active') - closed = count_by_state(milestones_by_state_and_title, 'closed') - all = milestones_by_state_and_title.map { |(_, title), _| title }.uniq.count + opened = count_by_state(project_milestones_by_state_and_title, 'active') + closed = count_by_state(project_milestones_by_state_and_title, 'closed') + all = project_milestones_by_state_and_title.map { |(_, title), _| title }.uniq.count { opened: opened, diff --git a/app/models/group.rb b/app/models/group.rb index f29e642ac91..70a4ceeffd8 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -18,6 +18,7 @@ class Group < Namespace has_many :requesters, -> { where.not(requested_at: nil) }, dependent: :destroy, as: :source, class_name: 'GroupMember' # rubocop:disable Cop/ActiveRecordDependent + has_many :milestones has_many :project_group_links, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent has_many :shared_projects, through: :project_group_links, source: :project has_many :notification_settings, dependent: :destroy, as: :source # rubocop:disable Cop/ActiveRecordDependent diff --git a/app/models/group_milestone.rb b/app/models/group_milestone.rb index 86d38e5468b..65249bd7bfc 100644 --- a/app/models/group_milestone.rb +++ b/app/models/group_milestone.rb @@ -16,4 +16,8 @@ class GroupMilestone < GlobalMilestone def issues_finder_params { group_id: group.id } end + + def is_legacy_group_milestone? + true + end end diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 2fc6191e785..6ea774470af 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -849,7 +849,10 @@ class MergeRequest < ActiveRecord::Base # def all_commit_shas if persisted? - merge_request_diffs.preload(:merge_request_diff_commits).flat_map(&:commit_shas).uniq + column_shas = MergeRequestDiffCommit.where(merge_request_diff: merge_request_diffs).pluck('DISTINCT(sha)') + serialised_shas = merge_request_diffs.where.not(st_commits: nil).flat_map(&:commit_shas) + + (column_shas + serialised_shas).uniq elsif compare_commits compare_commits.to_a.reverse.map(&:id) else diff --git a/app/models/milestone.rb b/app/models/milestone.rb index c0ccbf8e27e..48d00764965 100644 --- a/app/models/milestone.rb +++ b/app/models/milestone.rb @@ -18,17 +18,32 @@ class Milestone < ActiveRecord::Base cache_markdown_field :description belongs_to :project + belongs_to :group + has_many :issues has_many :labels, -> { distinct.reorder('labels.title') }, through: :issues has_many :merge_requests has_many :events, as: :target, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent + scope :of_projects, ->(ids) { where(project_id: ids) } + scope :of_groups, ->(ids) { where(group_id: ids) } scope :active, -> { with_state(:active) } scope :closed, -> { with_state(:closed) } - scope :of_projects, ->(ids) { where(project_id: ids) } + scope :for_projects, -> { where(group: nil).includes(:project) } + + scope :for_projects_and_groups, -> (project_ids, group_ids) do + conditions = [] + conditions << arel_table[:project_id].in(project_ids) if project_ids.compact.any? + conditions << arel_table[:group_id].in(group_ids) if group_ids.compact.any? + + where(conditions.reduce(:or)) + end + + validates :group, presence: true, unless: :project + validates :project, presence: true, unless: :group - validates :title, presence: true, uniqueness: { scope: :project_id } - validates :project, presence: true + validate :uniqueness_of_title, if: :title_changed? + validate :milestone_type_check validate :start_date_should_be_less_than_due_date, if: proc { |m| m.start_date.present? && m.due_date.present? } strip_attributes :title @@ -63,6 +78,14 @@ class Milestone < ActiveRecord::Base where(t[:title].matches(pattern).or(t[:description].matches(pattern))) end + + def filter_by_state(milestones, state) + case state + when 'closed' then milestones.closed + when 'all' then milestones + else milestones.active + end + end end def self.reference_prefix @@ -138,6 +161,8 @@ class Milestone < ActiveRecord::Base # Milestone.first.to_reference(same_namespace_project) # => "gitlab-ce%1" # def to_reference(from_project = nil, format: :iid, full: false) + return if is_group_milestone? + format_reference = milestone_format_reference(format) reference = "#{self.class.reference_prefix}#{format_reference}" @@ -152,6 +177,10 @@ class Milestone < ActiveRecord::Base id end + def for_display + self + end + def can_be_closed? active? && issues.opened.count.zero? end @@ -164,8 +193,45 @@ class Milestone < ActiveRecord::Base write_attribute(:title, sanitize_title(value)) if value.present? end + def safe_title + title.to_slug.normalize.to_s + end + + def parent + group || project + end + + def is_group_milestone? + group_id.present? + end + + def is_project_milestone? + project_id.present? + end + private + # Milestone titles must be unique across project milestones and group milestones + def uniqueness_of_title + if project + relation = Milestone.for_projects_and_groups([project_id], [project.group&.id]) + elsif group + project_ids = group.projects.map(&:id) + relation = Milestone.for_projects_and_groups(project_ids, [group.id]) + end + + title_exists = relation.find_by_title(title) + errors.add(:title, "already being used for another group or project milestone.") if title_exists + end + + # Milestone should be either a project milestone or a group milestone + def milestone_type_check + if group_id && project_id + field = project_id_changed? ? :project_id : :group_id + errors.add(field, "milestone should belong either to a project or a group.") + end + end + def milestone_format_reference(format = :iid) raise ArgumentError, 'Unknown format' unless [:iid, :name].include?(format) diff --git a/app/policies/ci/pipeline_schedule_policy.rb b/app/policies/ci/pipeline_schedule_policy.rb index 1877e89bb23..6b7598e1821 100644 --- a/app/policies/ci/pipeline_schedule_policy.rb +++ b/app/policies/ci/pipeline_schedule_policy.rb @@ -1,4 +1,14 @@ module Ci class PipelineSchedulePolicy < PipelinePolicy + alias_method :pipeline_schedule, :subject + + condition(:owner_of_schedule) do + can?(:developer_access) && pipeline_schedule.owned_by?(@user) + end + + rule { can?(:master_access) | owner_of_schedule }.policy do + enable :update_pipeline_schedule + enable :admin_pipeline_schedule + end end end diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb index 7cbca63fab4..323131c0f7e 100644 --- a/app/policies/project_policy.rb +++ b/app/policies/project_policy.rb @@ -162,7 +162,6 @@ class ProjectPolicy < BasePolicy enable :create_pipeline enable :update_pipeline enable :create_pipeline_schedule - enable :update_pipeline_schedule enable :create_merge_request enable :create_wiki enable :push_code @@ -188,7 +187,6 @@ class ProjectPolicy < BasePolicy enable :admin_build enable :admin_container_image enable :admin_pipeline - enable :admin_pipeline_schedule enable :admin_environment enable :admin_deployment enable :admin_pages diff --git a/app/serializers/label_entity.rb b/app/serializers/label_entity.rb index ad565654342..4452161051e 100644 --- a/app/serializers/label_entity.rb +++ b/app/serializers/label_entity.rb @@ -1,5 +1,6 @@ class LabelEntity < Grape::Entity - expose :id + expose :id, if: ->(label, _) { !label.is_a?(GlobalLabel) } + expose :title expose :color expose :description diff --git a/app/services/boards/create_service.rb b/app/services/boards/create_service.rb index 68f6a8619e5..9eedb9e65a2 100644 --- a/app/services/boards/create_service.rb +++ b/app/services/boards/create_service.rb @@ -1,19 +1,22 @@ module Boards class CreateService < BaseService def execute - if project.boards.empty? - create_board! - else - project.boards.first - end + create_board! if can_create_board? end private + def can_create_board? + project.boards.size == 0 + end + def create_board! - board = project.boards.create - board.lists.create(list_type: :backlog) - board.lists.create(list_type: :closed) + board = project.boards.create(params) + + if board.persisted? + board.lists.create(list_type: :backlog) + board.lists.create(list_type: :closed) + end board end diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb index 8dd0846f3bc..a03a7abfeb1 100644 --- a/app/services/issuable_base_service.rb +++ b/app/services/issuable_base_service.rb @@ -2,8 +2,11 @@ class IssuableBaseService < BaseService private def create_milestone_note(issuable) + milestone = issuable.milestone + return if milestone && milestone.is_group_milestone? + SystemNoteService.change_milestone( - issuable, issuable.project, current_user, issuable.milestone) + issuable, issuable.project, current_user, milestone) end def create_labels_note(issuable, old_labels) @@ -89,10 +92,12 @@ class IssuableBaseService < BaseService milestone_id = params[:milestone_id] return unless milestone_id - if milestone_id == IssuableFinder::NONE || - project.milestones.find_by(id: milestone_id).nil? - params[:milestone_id] = '' - end + params[:milestone_id] = '' if milestone_id == IssuableFinder::NONE + + milestone = + Milestone.for_projects_and_groups([project.id], [project.group&.id]).find_by_id(milestone_id) + + params[:milestone_id] = '' unless milestone end def filter_labels diff --git a/app/services/issues/move_service.rb b/app/services/issues/move_service.rb index 711f4035c55..29def25719d 100644 --- a/app/services/issues/move_service.rb +++ b/app/services/issues/move_service.rb @@ -61,8 +61,18 @@ module Issues end def cloneable_milestone_id - @new_project.milestones - .find_by(title: @old_issue.milestone.try(:title)).try(:id) + title = @old_issue.milestone&.title + return unless title + + if @new_project.group && can?(current_user, :read_group, @new_project.group) + group_id = @new_project.group.id + end + + params = + { title: title, project_ids: @new_project.id, group_ids: group_id } + + milestones = MilestonesFinder.new(params).execute + milestones.first&.id end def rewrite_notes diff --git a/app/services/milestones/base_service.rb b/app/services/milestones/base_service.rb index 176ab9f1ab5..4963601ea8b 100644 --- a/app/services/milestones/base_service.rb +++ b/app/services/milestones/base_service.rb @@ -1,4 +1,10 @@ module Milestones class BaseService < ::BaseService + # Parent can either a group or a project + attr_accessor :parent, :current_user, :params + + def initialize(parent, user, params = {}) + @parent, @current_user, @params = parent, user, params.dup + end end end diff --git a/app/services/milestones/close_service.rb b/app/services/milestones/close_service.rb index 608fc49d766..776ec4b287b 100644 --- a/app/services/milestones/close_service.rb +++ b/app/services/milestones/close_service.rb @@ -1,7 +1,7 @@ module Milestones class CloseService < Milestones::BaseService def execute(milestone) - if milestone.close + if milestone.close && milestone.is_project_milestone? event_service.close_milestone(milestone, current_user) end diff --git a/app/services/milestones/create_service.rb b/app/services/milestones/create_service.rb index b8e08c9f1eb..aef3124c7e3 100644 --- a/app/services/milestones/create_service.rb +++ b/app/services/milestones/create_service.rb @@ -1,9 +1,9 @@ module Milestones class CreateService < Milestones::BaseService def execute - milestone = project.milestones.new(params) + milestone = parent.milestones.new(params) - if milestone.save + if milestone.save && milestone.is_project_milestone? event_service.open_milestone(milestone, current_user) end diff --git a/app/services/milestones/reopen_service.rb b/app/services/milestones/reopen_service.rb index 573f9ee5c21..5b8b682caaf 100644 --- a/app/services/milestones/reopen_service.rb +++ b/app/services/milestones/reopen_service.rb @@ -1,7 +1,7 @@ module Milestones class ReopenService < Milestones::BaseService def execute(milestone) - if milestone.activate + if milestone.activate && milestone.is_project_milestone? event_service.reopen_milestone(milestone, current_user) end diff --git a/app/services/milestones/update_service.rb b/app/services/milestones/update_service.rb index ed64847f429..31b441ed476 100644 --- a/app/services/milestones/update_service.rb +++ b/app/services/milestones/update_service.rb @@ -5,9 +5,9 @@ module Milestones case state when 'activate' - Milestones::ReopenService.new(project, current_user, {}).execute(milestone) + Milestones::ReopenService.new(parent, current_user, {}).execute(milestone) when 'close' - Milestones::CloseService.new(project, current_user, {}).execute(milestone) + Milestones::CloseService.new(parent, current_user, {}).execute(milestone) end if params.present? diff --git a/app/services/quick_actions/interpret_service.rb b/app/services/quick_actions/interpret_service.rb index e4dfe87e614..6f82159e6c7 100644 --- a/app/services/quick_actions/interpret_service.rb +++ b/app/services/quick_actions/interpret_service.rb @@ -146,32 +146,6 @@ module QuickActions end end - desc do - "Change assignee#{'(s)' if issuable.allows_multiple_assignees?}" - end - explanation do |users| - users = issuable.allows_multiple_assignees? ? users : users.take(1) - "Change #{'assignee'.pluralize(users.size)} to #{users.map(&:to_reference).to_sentence}." - end - params do - issuable.allows_multiple_assignees? ? '@user1 @user2' : '@user' - end - condition do - issuable.persisted? && - current_user.can?(:"admin_#{issuable.to_ability_name}", project) - end - parse_params do |assignee_param| - extract_users(assignee_param) - end - command :reassign do |users| - @updates[:assignee_ids] = - if issuable.allows_multiple_assignees? - users.map(&:id) - else - [users.last.id] - end - end - desc 'Set milestone' explanation do |milestone| "Sets the milestone to #{milestone.to_reference}." if milestone diff --git a/app/validators/variable_duplicates_validator.rb b/app/validators/variable_duplicates_validator.rb new file mode 100644 index 00000000000..8a9d8892e9b --- /dev/null +++ b/app/validators/variable_duplicates_validator.rb @@ -0,0 +1,13 @@ +# VariableDuplicatesValidator +# +# This validtor is designed for especially the following condition +# - Use `accepts_nested_attributes_for :xxx` in a parent model +# - Use `validates :xxx, uniqueness: { scope: :xxx_id }` in a child model +class VariableDuplicatesValidator < ActiveModel::EachValidator + def validate_each(record, attribute, value) + duplicates = value.reject(&:marked_for_destruction?).group_by(&:key).select { |_, v| v.many? }.map(&:first) + if duplicates.any? + record.errors.add(attribute, "Duplicate variables: #{duplicates.join(", ")}") + end + end +end diff --git a/app/views/groups/milestones/_form.html.haml b/app/views/groups/milestones/_form.html.haml new file mode 100644 index 00000000000..7f450cd9a93 --- /dev/null +++ b/app/views/groups/milestones/_form.html.haml @@ -0,0 +1,27 @@ += form_for [@group, @milestone], html: { class: 'form-horizontal milestone-form common-note-form js-quick-submit js-requires-input' } do |f| + .row + = form_errors(@milestone) + + .col-md-6 + .form-group + = f.label :title, "Title", class: "control-label" + .col-sm-10 + = f.text_field :title, maxlength: 255, class: "form-control", required: true, autofocus: true + .form-group.milestone-description + = f.label :description, "Description", class: "control-label" + .col-sm-10 + = render layout: 'projects/md_preview', locals: { url: '' } do + = render 'projects/zen', f: f, attr: :description, classes: 'note-textarea', placeholder: 'Write milestone description...' + .clearfix + .error-alert + + = render "shared/milestones/form_dates", f: f + + .form-actions + - if @milestone.new_record? + = f.submit 'Create milestone', class: "btn-create btn" + = link_to "Cancel", group_milestones_path(@group), class: "btn btn-cancel" + - else + = f.submit 'Update milestone', class: "btn-create btn" + = link_to "Cancel", group_milestone_path(@group, @milestone), class: "btn btn-cancel" + diff --git a/app/views/groups/milestones/_milestone.html.haml b/app/views/groups/milestones/_milestone.html.haml index 4c4e0a26728..bae8997e24c 100644 --- a/app/views/groups/milestones/_milestone.html.haml +++ b/app/views/groups/milestones/_milestone.html.haml @@ -1,5 +1,6 @@ + = render 'shared/milestones/milestone', - milestone_path: group_milestone_path(@group, milestone.safe_title, title: milestone.title), + milestone_path: group_milestone_route(milestone), issues_path: issues_group_path(@group, milestone_title: milestone.title), merge_requests_path: merge_requests_group_path(@group, milestone_title: milestone.title), milestone: milestone diff --git a/app/views/groups/milestones/edit.html.haml b/app/views/groups/milestones/edit.html.haml new file mode 100644 index 00000000000..5f6d7d209d0 --- /dev/null +++ b/app/views/groups/milestones/edit.html.haml @@ -0,0 +1,7 @@ +- page_title "Milestones" +- render "header_title" + +%h3.page-title + Edit Milestone + += render "form" diff --git a/app/views/groups/milestones/index.html.haml b/app/views/groups/milestones/index.html.haml index f91bee0b610..6ceb4092307 100644 --- a/app/views/groups/milestones/index.html.haml +++ b/app/views/groups/milestones/index.html.haml @@ -9,11 +9,6 @@ = link_to new_group_milestone_path(@group), class: "btn btn-new" do New milestone -.row-content-block - Only milestones from - %strong= @group.name - group are listed here. - .milestones %ul.content-list - if @milestones.blank? diff --git a/app/views/groups/milestones/new.html.haml b/app/views/groups/milestones/new.html.haml index 7c7573862d0..e24844661ee 100644 --- a/app/views/groups/milestones/new.html.haml +++ b/app/views/groups/milestones/new.html.haml @@ -4,40 +4,4 @@ %h3.page-title New Milestone -%p.light - This will create milestone in every selected project -%hr - -= form_for @milestone, url: group_milestones_path(@group), html: { class: 'form-horizontal milestone-form common-note-form js-quick-submit js-requires-input' } do |f| - .row - - if @milestone.errors.any? - #error_explanation - .alert.alert-danger - %ul - - @milestone.errors.full_messages.each do |msg| - %li - = msg - - .col-md-6 - .form-group - = f.label :title, "Title", class: "control-label" - .col-sm-10 - = f.text_field :title, maxlength: 255, class: "form-control", required: true, autofocus: true - .form-group.milestone-description - = f.label :description, "Description", class: "control-label" - .col-sm-10 - = render layout: 'projects/md_preview', locals: { url: '' } do - = render 'projects/zen', f: f, attr: :description, classes: 'note-textarea', placeholder: 'Write milestone description...' - .clearfix - .error-alert - .form-group - = f.label :projects, "Projects", class: "control-label" - .col-sm-10 - = f.collection_select :project_ids, @group.projects.non_archived, :id, :name, - { selected: @group.projects.non_archived.pluck(:id) }, required: true, multiple: true, class: 'select2' - - = render "shared/milestones/form_dates", f: f - - .form-actions - = f.submit 'Create milestone', class: "btn-create btn" - = link_to "Cancel", group_milestones_path(@group), class: "btn btn-cancel" += render "form" diff --git a/app/views/groups/milestones/show.html.haml b/app/views/groups/milestones/show.html.haml index 33e68bc766e..54b1b7a734a 100644 --- a/app/views/groups/milestones/show.html.haml +++ b/app/views/groups/milestones/show.html.haml @@ -1,4 +1,4 @@ = render "header_title" = render 'shared/milestones/top', milestone: @milestone, group: @group -= render 'shared/milestones/tabs', milestone: @milestone, show_project_name: true += render 'shared/milestones/tabs', milestone: @milestone, show_project_name: true if @milestone.is_legacy_group_milestone? = render 'shared/milestones/sidebar', milestone: @milestone, affix_offset: 102 diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml index 14deb46eee3..fb90bb4b472 100644 --- a/app/views/layouts/nav/_project.html.haml +++ b/app/views/layouts/nav/_project.html.haml @@ -57,16 +57,17 @@ %span Snippets + - if project_nav_tab? :project_members + = nav_link(controller: :project_members) do + = link_to project_project_members_path(@project), title: 'Members', class: 'shortcuts-members' do + %span + Members + - if project_nav_tab? :settings = nav_link(path: %w[projects#edit members#show integrations#show services#edit repository#show ci_cd#show pages#show]) do = link_to edit_project_path(@project), title: 'Settings', class: 'shortcuts-tree' do %span Settings - - else - = nav_link(path: %w[members#show]) do - = link_to project_settings_members_path(@project), title: 'Settings', class: 'shortcuts-tree' do - %span - Settings -# Shortcut to Project > Activity %li.hidden diff --git a/app/views/projects/pipeline_schedules/_form.html.haml b/app/views/projects/pipeline_schedules/_form.html.haml index fc7fa5c1876..857ae00d0ab 100644 --- a/app/views/projects/pipeline_schedules/_form.html.haml +++ b/app/views/projects/pipeline_schedules/_form.html.haml @@ -24,6 +24,14 @@ = f.text_field :ref, value: @schedule.ref, id: 'schedule_ref', class: 'hidden', name: 'schedule[ref]', required: true .form-group .col-md-9 + %label.label-light + #{ s_('PipelineSchedules|Variables') } + %ul.js-pipeline-variable-list.pipeline-variable-list + - @schedule.variables.each do |variable| + = render 'variable_row', id: variable.id, key: variable.key, value: variable.value + = render 'variable_row' + .form-group + .col-md-9 = f.label :active, s_('PipelineSchedules|Activated'), class: 'label-light' %div = f.check_box :active, required: false, value: @schedule.active? diff --git a/app/views/projects/pipeline_schedules/_pipeline_schedule.html.haml b/app/views/projects/pipeline_schedules/_pipeline_schedule.html.haml index 08ccd57c81a..97c0407a01d 100644 --- a/app/views/projects/pipeline_schedules/_pipeline_schedule.html.haml +++ b/app/views/projects/pipeline_schedules/_pipeline_schedule.html.haml @@ -26,7 +26,7 @@ = pipeline_schedule.owner&.name %td .pull-right.btn-group - - if can?(current_user, :update_pipeline_schedule, @project) && !pipeline_schedule.owned_by?(current_user) + - if can?(current_user, :update_pipeline_schedule, pipeline_schedule) = link_to take_ownership_pipeline_schedule_path(pipeline_schedule), method: :post, title: s_('PipelineSchedules|Take ownership'), class: 'btn' do = s_('PipelineSchedules|Take ownership') - if can?(current_user, :update_pipeline_schedule, pipeline_schedule) diff --git a/app/views/projects/pipeline_schedules/_variable_row.html.haml b/app/views/projects/pipeline_schedules/_variable_row.html.haml new file mode 100644 index 00000000000..564cb5d1ca9 --- /dev/null +++ b/app/views/projects/pipeline_schedules/_variable_row.html.haml @@ -0,0 +1,17 @@ +- id = local_assigns.fetch(:id, nil) +- key = local_assigns.fetch(:key, "") +- value = local_assigns.fetch(:value, "") +%li.js-row.pipeline-variable-row{ data: { is_persisted: "#{!id.nil?}" } } + .pipeline-variable-row-body + %input{ type: "hidden", name: "schedule[variables_attributes][][id]", value: id } + %input.js-destroy-input{ type: "hidden", name: "schedule[variables_attributes][][_destroy]" } + %input.js-user-input.pipeline-variable-key-input.form-control{ type: "text", + name: "schedule[variables_attributes][][key]", + value: key, + placeholder: s_('PipelineSchedules|Input variable key') } + %textarea.js-user-input.pipeline-variable-value-input.form-control{ rows: 1, + name: "schedule[variables_attributes][][value]", + placeholder: s_('PipelineSchedules|Input variable value') } + = value + %button.js-row-remove-button.pipeline-variable-row-remove-button{ 'aria-label': s_('PipelineSchedules|Remove variable row') } + %i.fa.fa-minus-circle{ 'aria-hidden': "true" } diff --git a/app/views/projects/project_members/_team.html.haml b/app/views/projects/project_members/_team.html.haml index 7ed467c8841..1f18490594b 100644 --- a/app/views/projects/project_members/_team.html.haml +++ b/app/views/projects/project_members/_team.html.haml @@ -5,7 +5,7 @@ %strong #{@project.name} %span.badge= @project_members.total_count - = form_tag project_settings_members_path(@project), method: :get, class: 'form-inline member-search-form flex-project-members-form' do + = form_tag project_project_members_path(@project), method: :get, class: 'form-inline member-search-form flex-project-members-form' do .form-group = search_field_tag :search, params[:search], { placeholder: 'Find existing members by name', class: 'form-control', spellcheck: false } %button.member-search-btn{ type: "submit", "aria-label" => "Submit search" } diff --git a/app/views/projects/project_members/import.html.haml b/app/views/projects/project_members/import.html.haml index 03b33eb2da7..f6ca8d5a921 100644 --- a/app/views/projects/project_members/import.html.haml +++ b/app/views/projects/project_members/import.html.haml @@ -12,4 +12,4 @@ .form-actions = button_tag 'Import project members', class: "btn btn-create" - = link_to "Cancel", project_settings_members_path(@project), class: "btn btn-cancel" + = link_to "Cancel", project_project_members_path(@project), class: "btn btn-cancel" diff --git a/app/views/projects/project_members/_index.html.haml b/app/views/projects/project_members/index.html.haml index 08e2d6177ab..25153fd0b6f 100644 --- a/app/views/projects/project_members/_index.html.haml +++ b/app/views/projects/project_members/index.html.haml @@ -1,3 +1,5 @@ +- page_title "Members" + .row.prepend-top-default .col-lg-12 %h4 diff --git a/app/views/projects/settings/_head.html.haml b/app/views/projects/settings/_head.html.haml index b5773acb5a4..15ba09b10ba 100644 --- a/app/views/projects/settings/_head.html.haml +++ b/app/views/projects/settings/_head.html.haml @@ -9,10 +9,6 @@ = link_to edit_project_path(@project), title: 'General' do %span General - = nav_link(controller: :members) do - = link_to project_settings_members_path(@project), title: 'Members' do - %span - Members - if can_edit = nav_link(controller: [:integrations, :services, :hooks, :hook_logs]) do = link_to project_settings_integrations_path(@project), title: 'Integrations' do diff --git a/app/views/shared/milestones/_issuable.html.haml b/app/views/shared/milestones/_issuable.html.haml index a7c67ac9980..3739f4c221d 100644 --- a/app/views/shared/milestones/_issuable.html.haml +++ b/app/views/shared/milestones/_issuable.html.haml @@ -1,14 +1,13 @@ -# @project is present when viewing Project's milestone - project = @project || issuable.project - namespace = @project_namespace || project.namespace.becomes(Namespace) +- labels = issuable.labels - assignees = issuable.assignees -- issuable_type = issuable.class.table_name - base_url_args = [namespace, project] -- issuable_type_args = base_url_args + [issuable_type] +- issuable_type_args = base_url_args + [issuable.class.table_name] - issuable_url_args = base_url_args + [issuable] -- can_update = can?(current_user, :"update_#{issuable.to_ability_name}", issuable) -%li{ id: dom_id(issuable, 'sortable'), class: "issuable-row #{'is-disabled' unless can_update}", 'data-iid' => issuable.iid, 'data-id' => issuable.id, 'data-url' => polymorphic_path(issuable_url_args) } +%li.issuable-row %span - if show_project_name %strong #{project.name} · @@ -18,10 +17,10 @@ = confidential_icon(issuable) = link_to issuable.title, issuable_url_args, title: issuable.title .issuable-detail - = link_to [project.namespace.becomes(Namespace), project, issuable] do + = link_to [namespace, project, issuable] do %span.issuable-number= issuable.to_reference - - issuable.labels.each do |label| + - labels.each do |label| = link_to polymorphic_path(issuable_type_args, { milestone_title: @milestone.title, label_name: label.title, state: 'all' }) do - render_colored_label(label) diff --git a/app/views/shared/milestones/_milestone.html.haml b/app/views/shared/milestones/_milestone.html.haml index ecc8b42979c..6f6a036b13f 100644 --- a/app/views/shared/milestones/_milestone.html.haml +++ b/app/views/shared/milestones/_milestone.html.haml @@ -1,10 +1,15 @@ - dashboard = local_assigns[:dashboard] -- custom_dom_id = dom_id(@project ? milestone : milestone.milestones.first) +- custom_dom_id = dom_id(milestone.try(:milestones) ? milestone.milestones.first : milestone) %li{ class: "milestone milestone-#{milestone.closed? ? 'closed' : 'open'}", id: custom_dom_id } .row .col-sm-6 %strong= link_to truncate(milestone.title, length: 100), milestone_path + - if milestone.is_group_milestone? + %span - Group Milestone + - else + %span - Project Milestone + .col-sm-6 .pull-right.light #{milestone.percent_complete(current_user)}% complete .row @@ -13,26 +18,32 @@ · = link_to pluralize(milestone.merge_requests.size, 'Merge Request'), merge_requests_path .col-sm-6= milestone_progress_bar(milestone) - - if milestone.is_a?(GlobalMilestone) + - if milestone.is_a?(GlobalMilestone) || milestone.is_group_milestone? .row .col-sm-6 - .expiration= render('shared/milestone_expired', milestone: milestone) - .projects - - milestone.milestones.each do |milestone| - = link_to milestone_path(milestone) do - %span.label.label-gray - = dashboard ? milestone.project.name_with_namespace : milestone.project.name + - if milestone.is_legacy_group_milestone? + .expiration= render('shared/milestone_expired', milestone: milestone) + .projects + - milestone.milestones.each do |milestone| + = link_to milestone_path(milestone) do + %span.label.label-gray + = dashboard ? milestone.project.name_with_namespace : milestone.project.name - if @group - .col-sm-6 + .col-sm-6.milestone-actions - if can?(current_user, :admin_milestones, @group) + - if milestone.is_group_milestone? + = link_to edit_group_milestone_path(@group, milestone.id), class: "btn btn-xs btn-grouped" do + Edit + \ - if milestone.closed? - = link_to 'Reopen Milestone', group_milestone_path(@group, milestone.safe_title, title: milestone.title, milestone: {state_event: :activate }), method: :put, class: "btn btn-xs btn-grouped btn-reopen" + = link_to 'Reopen Milestone', group_milestone_route(milestone, {state_event: :activate }), method: :put, class: "btn btn-xs btn-grouped btn-reopen" - else - = link_to 'Close Milestone', group_milestone_path(@group, milestone.safe_title, title: milestone.title, milestone: {state_event: :close }), method: :put, class: "btn btn-xs btn-close" + = link_to 'Close Milestone', group_milestone_route(milestone, {state_event: :close }), method: :put, class: "btn btn-xs btn-grouped btn-close" - if @project .row - .col-sm-6= render('shared/milestone_expired', milestone: milestone) + .col-sm-6 + = render('shared/milestone_expired', milestone: milestone) .col-sm-6.milestone-actions - if can?(current_user, :admin_milestone, milestone.project) and milestone.active? = link_to edit_project_milestone_path(milestone.project, milestone), class: "btn btn-xs btn-grouped" do diff --git a/app/views/shared/milestones/_tabs.html.haml b/app/views/shared/milestones/_tabs.html.haml index e2d1695b7c3..b95a4ea674d 100644 --- a/app/views/shared/milestones/_tabs.html.haml +++ b/app/views/shared/milestones/_tabs.html.haml @@ -1,8 +1,10 @@ +- issues_accessible = milestone.is_a?(GlobalMilestone) || can?(current_user, :read_issue, @project) + .scrolling-tabs-container.inner-page-scroll-tabs.is-smaller .fade-left= icon('angle-left') .fade-right= icon('angle-right') %ul.nav-links.scrolling-tabs.js-milestone-tabs - - if milestone.is_a?(GlobalMilestone) || can?(current_user, :read_issue, @project) + - if issues_accessible %li.active = link_to '#tab-issues', 'data-toggle' => 'tab', 'data-show' => '.tab-issues-buttons' do Issues @@ -25,13 +27,14 @@ Labels %span.badge= milestone.labels.count +- issues = milestone.sorted_issues(current_user) - show_project_name = local_assigns.fetch(:show_project_name, false) - show_full_project_name = local_assigns.fetch(:show_full_project_name, false) .tab-content.milestone-content - - if milestone.is_a?(GlobalMilestone) || can?(current_user, :read_issue, @project) + - if issues_accessible .tab-pane.active#tab-issues{ data: { sort_endpoint: (sort_issues_project_milestone_path(@project, @milestone) if @project && current_user) } } - = render 'shared/milestones/issues_tab', issues: milestone.sorted_issues(current_user), show_project_name: show_project_name, show_full_project_name: show_full_project_name + = render 'shared/milestones/issues_tab', issues: issues, show_project_name: show_project_name, show_full_project_name: show_full_project_name .tab-pane#tab-merge-requests -# loaded async = render "shared/milestones/tab_loading" diff --git a/app/views/shared/milestones/_top.html.haml b/app/views/shared/milestones/_top.html.haml index 20a12613cfc..b93837e3087 100644 --- a/app/views/shared/milestones/_top.html.haml +++ b/app/views/shared/milestones/_top.html.haml @@ -22,39 +22,55 @@ - if group .pull-right - if can?(current_user, :admin_milestones, group) + - if milestone.is_group_milestone? + = link_to edit_group_milestone_path(group, milestone.iid), class: "btn btn btn-grouped" do + Edit - if milestone.active? - = link_to 'Close Milestone', group_milestone_path(group, milestone.safe_title, title: milestone.title, milestone: {state_event: :close }), method: :put, class: "btn btn-grouped btn-close" + = link_to 'Close Milestone', group_milestone_route(milestone, {state_event: :close }), method: :put, class: "btn btn-grouped btn-close" - else - = link_to 'Reopen Milestone', group_milestone_path(group, milestone.safe_title, title: milestone.title, milestone: {state_event: :activate }), method: :put, class: "btn btn-grouped btn-reopen" + = link_to 'Reopen Milestone', group_milestone_route(milestone, {state_event: :activate }), method: :put, class: "btn btn-grouped btn-reopen" .detail-page-description.milestone-detail %h2.title = markdown_field(milestone, :title) + - if @milestone.is_group_milestone? && @milestone.description.present? + %div + .description + .wiki + = markdown_field(@milestone, :description) - if milestone.complete?(current_user) && milestone.active? .alert.alert-success.prepend-top-default - close_msg = group ? 'You may close the milestone now.' : 'Navigate to the project to close the milestone.' %span All issues for this milestone are closed. #{close_msg} -.table-holder - %table.table - %thead - %tr - %th Project - %th Open issues - %th State - %th Due date - - milestone.milestones.each do |ms| - %tr - %td - - project_name = group ? ms.project.name : ms.project.name_with_namespace - = link_to project_name, project_milestone_path(ms.project, ms) - %td - = ms.issues_visible_to_user(current_user).opened.count - %td - - if ms.closed? - Closed - - else - Open - %td - = ms.expires_at +- if @milestone.is_legacy_group_milestone? || @milestone.is_dashboard_milestone? + .table-holder + %table.table + %thead + %tr + %th Project + %th Open issues + %th State + %th Due date + - milestone.milestones.each do |ms| + %tr + %td + - project_name = group ? ms.project.name : ms.project.name_with_namespace + = link_to project_name, project_milestone_path(ms.project, ms) + %td + = ms.issues_visible_to_user(current_user).opened.count + %td + - if ms.closed? + Closed + - else + Open + %td + = ms.expires_at +- elsif @milestone.is_group_milestone? + %br + View + = link_to 'Issues', issues_group_path(@group, milestone_title: milestone.title) + or + = link_to 'Merge Requests', merge_requests_group_path(@group, milestone_title: milestone.title) + in this milestone |