diff options
Diffstat (limited to 'app')
31 files changed, 423 insertions, 193 deletions
diff --git a/app/assets/javascripts/issuable_sidebar/components/sidebar_app.vue b/app/assets/javascripts/issuable_sidebar/components/sidebar_app.vue new file mode 100644 index 00000000000..06c50f62aab --- /dev/null +++ b/app/assets/javascripts/issuable_sidebar/components/sidebar_app.vue @@ -0,0 +1,23 @@ +<script> +export default { + props: { + signedIn: { + type: Boolean, + required: true, + }, + sidebarStatusClass: { + type: String, + required: false, + default: '', + }, + }, +}; +</script> + +<template> + <aside + :class="sidebarStatusClass" + class="right-sidebar js-right-sidebar js-issuable-sidebar" + aria-live="polite" + ></aside> +</template> diff --git a/app/assets/javascripts/issuable_sidebar/sidebar_bundle.js b/app/assets/javascripts/issuable_sidebar/sidebar_bundle.js new file mode 100644 index 00000000000..c8acafa8cd8 --- /dev/null +++ b/app/assets/javascripts/issuable_sidebar/sidebar_bundle.js @@ -0,0 +1,27 @@ +import Vue from 'vue'; + +import SidebarApp from './components/sidebar_app.vue'; + +export default () => { + const el = document.getElementById('js-vue-issuable-sidebar'); + + if (!el) { + return false; + } + + const { sidebarStatusClass } = el.dataset; + // An empty string is present when user is signed in. + const signedIn = el.dataset.signedIn === ''; + + return new Vue({ + el, + components: { SidebarApp }, + render: createElement => + createElement('sidebar-app', { + props: { + signedIn, + sidebarStatusClass, + }, + }), + }); +}; diff --git a/app/assets/javascripts/jobs/components/log/duration_badge.vue b/app/assets/javascripts/jobs/components/log/duration_badge.vue index 31a101d2c95..8e5dcdcc902 100644 --- a/app/assets/javascripts/jobs/components/log/duration_badge.vue +++ b/app/assets/javascripts/jobs/components/log/duration_badge.vue @@ -9,7 +9,7 @@ export default { }; </script> <template> - <div class="log-duration-badge rounded align-self-start px-2 ml-2 flex-shrink-0"> + <div class="log-duration-badge rounded align-self-start px-2 ml-2 flex-shrink-0 ws-normal"> {{ duration }} </div> </template> diff --git a/app/assets/javascripts/jobs/components/log/line.vue b/app/assets/javascripts/jobs/components/log/line.vue index 9fae541125e..33ee84bd4ee 100644 --- a/app/assets/javascripts/jobs/components/log/line.vue +++ b/app/assets/javascripts/jobs/components/log/line.vue @@ -21,8 +21,12 @@ export default { <template> <div class="js-line log-line"> <line-number :line-number="line.lineNumber" :path="path" /> - <span v-for="(content, i) in line.content" :key="i" :class="content.style">{{ - content.text - }}</span> + <span + v-for="(content, i) in line.content" + :key="i" + :class="content.style" + class="ws-pre-wrap" + >{{ content.text }}</span + > </div> </template> diff --git a/app/assets/javascripts/jobs/components/log/line_header.vue b/app/assets/javascripts/jobs/components/log/line_header.vue index 92cf3b3cf5f..85ccd5996b5 100644 --- a/app/assets/javascripts/jobs/components/log/line_header.vue +++ b/app/assets/javascripts/jobs/components/log/line_header.vue @@ -43,15 +43,19 @@ export default { <template> <div - class="log-line collapsible-line d-flex justify-content-between" + class="log-line collapsible-line d-flex justify-content-between ws-normal" role="button" @click="handleOnClick" > <icon :name="iconName" class="arrow position-absolute" /> <line-number :line-number="line.lineNumber" :path="path" /> - <span v-for="(content, i) in line.content" :key="i" class="line-text" :class="content.style">{{ - content.text - }}</span> + <span + v-for="(content, i) in line.content" + :key="i" + class="line-text w-100 ws-pre-wrap" + :class="content.style" + >{{ content.text }}</span + > <duration-badge v-if="duration" :duration="duration" /> </div> </template> diff --git a/app/assets/javascripts/jobs/components/log/line_number.vue b/app/assets/javascripts/jobs/components/log/line_number.vue index 08c4a7ed330..ae96c32874b 100644 --- a/app/assets/javascripts/jobs/components/log/line_number.vue +++ b/app/assets/javascripts/jobs/components/log/line_number.vue @@ -48,7 +48,7 @@ export default { <template> <gl-link :id="lineNumberId" - class="d-inline-block text-right line-number" + class="d-inline-block text-right line-number flex-shrink-0" :href="buildLineNumber" >{{ parsedLineNumber }}</gl-link > diff --git a/app/assets/javascripts/jobs/store/mutations.js b/app/assets/javascripts/jobs/store/mutations.js index 412ae146ca0..702f00888d0 100644 --- a/app/assets/javascripts/jobs/store/mutations.js +++ b/app/assets/javascripts/jobs/store/mutations.js @@ -19,7 +19,7 @@ export default { state.isSidebarOpen = true; }, - [types.RECEIVE_TRACE_SUCCESS](state, log) { + [types.RECEIVE_TRACE_SUCCESS](state, log = {}) { if (log.state) { state.traceState = log.state; } diff --git a/app/assets/javascripts/monitoring/components/dashboard.vue b/app/assets/javascripts/monitoring/components/dashboard.vue index 9ecb9324f8c..09afa16e283 100644 --- a/app/assets/javascripts/monitoring/components/dashboard.vue +++ b/app/assets/javascripts/monitoring/components/dashboard.vue @@ -416,7 +416,6 @@ export default { <gl-button v-if="showRearrangePanelsBtn" :pressed="isRearrangingPanels" - new-style variant="default" class="mr-2 mt-1 js-rearrange-button" @click="toggleRearrangingPanels" @@ -426,7 +425,6 @@ export default { <gl-button v-if="addingMetricsAvailable" v-gl-modal="$options.addMetric.modalId" - new-style variant="outline-success" class="mr-2 mt-1 js-add-metric-button" > diff --git a/app/assets/javascripts/pages/projects/issues/show.js b/app/assets/javascripts/pages/projects/issues/show.js index 0447d1f79fb..28a136a5fa5 100644 --- a/app/assets/javascripts/pages/projects/issues/show.js +++ b/app/assets/javascripts/pages/projects/issues/show.js @@ -5,6 +5,7 @@ import ZenMode from '~/zen_mode'; import '~/notes/index'; import initIssueableApp from '~/issue_show'; import initRelatedMergeRequestsApp from '~/related_merge_requests'; +import initVueIssuableSidebarApp from '~/issuable_sidebar/sidebar_bundle'; export default function() { initIssueableApp(); @@ -12,5 +13,9 @@ export default function() { new Issue(); // eslint-disable-line no-new new ShortcutsIssuable(); // eslint-disable-line no-new new ZenMode(); // eslint-disable-line no-new - initIssuableSidebar(); + if (gon.features && gon.features.vueIssuableSidebar) { + initVueIssuableSidebarApp(); + } else { + initIssuableSidebar(); + } } diff --git a/app/assets/javascripts/pages/projects/issues/show/index.js b/app/assets/javascripts/pages/projects/issues/show/index.js index 7968dfd7a12..ce74a6de11f 100644 --- a/app/assets/javascripts/pages/projects/issues/show/index.js +++ b/app/assets/javascripts/pages/projects/issues/show/index.js @@ -3,5 +3,7 @@ import initShow from '../show'; document.addEventListener('DOMContentLoaded', () => { initShow(); - initSidebarBundle(); + if (gon.features && !gon.features.vueIssuableSidebar) { + initSidebarBundle(); + } }); diff --git a/app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js b/app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js index 7bfb83a2204..fa1de1f13cb 100644 --- a/app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js +++ b/app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js @@ -4,11 +4,16 @@ import ShortcutsIssuable from '~/behaviors/shortcuts/shortcuts_issuable'; import { handleLocationHash } from '~/lib/utils/common_utils'; import howToMerge from '~/how_to_merge'; import initPipelines from '~/commit/pipelines/pipelines_bundle'; +import initVueIssuableSidebarApp from '~/issuable_sidebar/sidebar_bundle'; import initWidget from '../../../vue_merge_request_widget'; export default function() { new ZenMode(); // eslint-disable-line no-new - initIssuableSidebar(); + if (gon.features && gon.features.vueIssuableSidebar) { + initVueIssuableSidebarApp(); + } else { + initIssuableSidebar(); + } initPipelines(); new ShortcutsIssuable(true); // eslint-disable-line no-new handleLocationHash(); diff --git a/app/assets/javascripts/pages/projects/merge_requests/show/index.js b/app/assets/javascripts/pages/projects/merge_requests/show/index.js index f61f4db78d5..ddc648702f1 100644 --- a/app/assets/javascripts/pages/projects/merge_requests/show/index.js +++ b/app/assets/javascripts/pages/projects/merge_requests/show/index.js @@ -4,6 +4,8 @@ import initShow from '../init_merge_request_show'; document.addEventListener('DOMContentLoaded', () => { initShow(); - initSidebarBundle(); + if (gon.features && !gon.features.vueIssuableSidebar) { + initSidebarBundle(); + } initMrNotes(); }); diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss index 16cb63fc0df..370fc84e492 100644 --- a/app/assets/stylesheets/framework/common.scss +++ b/app/assets/stylesheets/framework/common.scss @@ -440,6 +440,7 @@ img.emoji { .flex-no-shrink { flex-shrink: 0; } .ws-initial { white-space: initial; } .ws-normal { white-space: normal; } +.ws-pre-wrap { white-space: pre-wrap; } .overflow-auto { overflow: auto; } .d-flex-center { diff --git a/app/assets/stylesheets/framework/job_log.scss b/app/assets/stylesheets/framework/job_log.scss index f26c475c3c1..4a57a458c50 100644 --- a/app/assets/stylesheets/framework/job_log.scss +++ b/app/assets/stylesheets/framework/job_log.scss @@ -9,7 +9,6 @@ border-radius: $border-radius-small; min-height: 42px; background-color: $builds-trace-bg; - white-space: pre-wrap; } .log-line { diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb index 3204e1e388b..35e364abba3 100644 --- a/app/controllers/groups_controller.rb +++ b/app/controllers/groups_controller.rb @@ -104,7 +104,6 @@ class GroupsController < Groups::ApplicationController redirect_to edit_group_path(@group, anchor: params[:update_section]), notice: "Group '#{@group.name}' was successfully updated." else @group.path = @group.path_before_last_save || @group.path_was - render action: "edit" end end @@ -124,7 +123,7 @@ class GroupsController < Groups::ApplicationController flash[:notice] = "Group '#{@group.name}' was successfully transferred." redirect_to group_path(@group) else - flash[:alert] = service.error + flash[:alert] = service.error.html_safe redirect_to edit_group_path(@group) end end diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index 7a192a9ec2d..96cb400950b 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -42,6 +42,10 @@ class Projects::IssuesController < Projects::ApplicationController before_action :authorize_import_issues!, only: [:import_csv] before_action :authorize_download_code!, only: [:related_branches] + before_action do + push_frontend_feature_flag(:vue_issuable_sidebar, project.group) + end + respond_to :html alias_method :designs, :show diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index bd22226da5c..ff199e05e99 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -21,6 +21,10 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo push_frontend_feature_flag(:diffs_batch_load, @project) end + before_action do + push_frontend_feature_flag(:vue_issuable_sidebar, @project.group) + end + around_action :allow_gitaly_ref_name_caching, only: [:index, :show, :discussions] def index diff --git a/app/controllers/projects/pipelines_controller.rb b/app/controllers/projects/pipelines_controller.rb index 1bacdc0b1b2..106ef1b72c1 100644 --- a/app/controllers/projects/pipelines_controller.rb +++ b/app/controllers/projects/pipelines_controller.rb @@ -184,7 +184,7 @@ class Projects::PipelinesController < Projects::ApplicationController end def show_represent_params - { grouped: true } + { grouped: true, expanded: params[:expanded].to_a.map(&:to_i) } end def create_params diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 3dacd6a6224..b016aa8e477 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -42,6 +42,7 @@ module Ci has_many :job_artifacts, class_name: 'Ci::JobArtifact', foreign_key: :job_id, dependent: :destroy, inverse_of: :job # rubocop:disable Cop/ActiveRecordDependent has_many :job_variables, class_name: 'Ci::JobVariable', foreign_key: :job_id + has_many :sourced_pipelines, class_name: 'Ci::Sources::Pipeline', foreign_key: :source_job_id Ci::JobArtifact.file_types.each do |key, value| has_one :"job_artifacts_#{key}", -> { where(file_type: value) }, class_name: 'Ci::JobArtifact', inverse_of: :job, foreign_key: :job_id diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index b34fd3f1ec9..5e52062ef40 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -52,9 +52,15 @@ module Ci has_many :auto_canceled_pipelines, class_name: 'Ci::Pipeline', foreign_key: 'auto_canceled_by_id' has_many :auto_canceled_jobs, class_name: 'CommitStatus', foreign_key: 'auto_canceled_by_id' + has_many :sourced_pipelines, class_name: 'Ci::Sources::Pipeline', foreign_key: :source_pipeline_id + has_one :source_pipeline, class_name: 'Ci::Sources::Pipeline', inverse_of: :pipeline has_one :chat_data, class_name: 'Ci::PipelineChatData' + has_many :triggered_pipelines, through: :sourced_pipelines, source: :pipeline + has_one :triggered_by_pipeline, through: :source_pipeline, source: :source_pipeline + has_one :source_job, through: :source_pipeline, source: :source_job + accepts_nested_attributes_for :variables, reject_if: :persisted? delegate :id, to: :project, prefix: true diff --git a/app/models/ci/pipeline_enums.rb b/app/models/ci/pipeline_enums.rb index cb92aef4bda..859abc4a0d5 100644 --- a/app/models/ci/pipeline_enums.rb +++ b/app/models/ci/pipeline_enums.rb @@ -22,6 +22,7 @@ module Ci schedule: 4, api: 5, external: 6, + pipeline: 7, chat: 8, merge_request_event: 10, external_pull_request_event: 11 diff --git a/app/models/ci/sources/pipeline.rb b/app/models/ci/sources/pipeline.rb new file mode 100644 index 00000000000..feaec27281c --- /dev/null +++ b/app/models/ci/sources/pipeline.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +module Ci + module Sources + class Pipeline < ApplicationRecord + self.table_name = "ci_sources_pipelines" + + belongs_to :project, class_name: "Project" + belongs_to :pipeline, class_name: "Ci::Pipeline", inverse_of: :source_pipeline + + belongs_to :source_project, class_name: "Project", foreign_key: :source_project_id + belongs_to :source_job, class_name: "CommitStatus", foreign_key: :source_job_id + belongs_to :source_pipeline, class_name: "Ci::Pipeline", foreign_key: :source_pipeline_id + + validates :project, presence: true + validates :pipeline, presence: true + + validates :source_project, presence: true + validates :source_job, presence: true + validates :source_pipeline, presence: true + end + end +end + +::Ci::Sources::Pipeline.prepend_if_ee('::EE::Ci::Sources::Pipeline') diff --git a/app/models/group.rb b/app/models/group.rb index 0501fe94440..8b21206fccf 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -259,6 +259,10 @@ class Group < Namespace members_with_parents.maintainers.exists?(user_id: user) end + def has_container_repositories? + container_repositories.exists? + end + # @deprecated alias_method :has_master?, :has_maintainer? diff --git a/app/models/project.rb b/app/models/project.rb index d7e3dc676ca..4d518862146 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -297,6 +297,9 @@ class Project < ApplicationRecord has_many :external_pull_requests, inverse_of: :project + has_many :sourced_pipelines, class_name: 'Ci::Sources::Pipeline', foreign_key: :source_project_id + has_many :source_pipelines, class_name: 'Ci::Sources::Pipeline', foreign_key: :project_id + has_one :pages_metadatum, class_name: 'ProjectPagesMetadatum', inverse_of: :project accepts_nested_attributes_for :variables, allow_destroy: true diff --git a/app/serializers/pipeline_details_entity.rb b/app/serializers/pipeline_details_entity.rb index 808e87c3fcf..71589ac8315 100644 --- a/app/serializers/pipeline_details_entity.rb +++ b/app/serializers/pipeline_details_entity.rb @@ -10,6 +10,7 @@ class PipelineDetailsEntity < PipelineEntity expose :manual_actions, using: BuildActionEntity expose :scheduled_actions, using: BuildActionEntity end -end -PipelineDetailsEntity.prepend_if_ee('EE::PipelineDetailsEntity') + expose :triggered_by_pipeline, as: :triggered_by, with: TriggeredPipelineEntity + expose :triggered_pipelines, as: :triggered, using: TriggeredPipelineEntity +end diff --git a/app/serializers/pipeline_serializer.rb b/app/serializers/pipeline_serializer.rb index eaaeaf040a2..fc3160e3c69 100644 --- a/app/serializers/pipeline_serializer.rb +++ b/app/serializers/pipeline_serializer.rb @@ -54,9 +54,9 @@ class PipelineSerializer < BaseSerializer artifacts: { project: [:route, { namespace: :route }] } - } + }, + { triggered_by_pipeline: [:project, :user] }, + { triggered_pipelines: [:project, :user] } ] end end - -PipelineSerializer.prepend_if_ee('EE::PipelineSerializer') diff --git a/app/serializers/triggered_pipeline_entity.rb b/app/serializers/triggered_pipeline_entity.rb new file mode 100644 index 00000000000..fd7e4454abf --- /dev/null +++ b/app/serializers/triggered_pipeline_entity.rb @@ -0,0 +1,65 @@ +# frozen_string_literal: true + +class TriggeredPipelineEntity < Grape::Entity + include RequestAwareEntity + + MAX_EXPAND_DEPTH = 3 + + expose :id + expose :user, using: UserEntity + expose :active?, as: :active + expose :coverage + expose :source + + expose :path do |pipeline| + project_pipeline_path(pipeline.project, pipeline) + end + + expose :details do + expose :detailed_status, as: :status, with: DetailedStatusEntity + + expose :ordered_stages, + as: :stages, using: StageEntity, + if: -> (_, opts) { can_read_details? && expand?(opts) } + end + + expose :triggered_by_pipeline, + as: :triggered_by, with: TriggeredPipelineEntity, + if: -> (_, opts) { can_read_details? && expand_for_path?(opts) } + + expose :triggered_pipelines, + as: :triggered, using: TriggeredPipelineEntity, + if: -> (_, opts) { can_read_details? && expand_for_path?(opts) } + + expose :project, using: ProjectEntity + + private + + alias_method :pipeline, :object + + def can_read_details? + can?(request.current_user, :read_pipeline, pipeline) + end + + def detailed_status + pipeline.detailed_status(request.current_user) + end + + def expand?(opts) + opts[:expanded].to_a.include?(pipeline.id) + end + + def expand_for_path?(opts) + # The `opts[:attr_path]` holds a list of all `exposes` in path + # The check ensures that we always expand only `triggered_by`, `triggered_by`, ... + # but not the `triggered_by`, `triggered` which would result in dead loop + attr_path = opts[:attr_path] + current_expose = attr_path.last + + # We expand at most to depth of MAX_DEPTH + # We ensure that we expand in one direction: triggered_by,... or triggered, ... + attr_path.length < MAX_EXPAND_DEPTH && + attr_path.all?(current_expose) && + expand?(opts) + end +end diff --git a/app/services/ci/pipeline_trigger_service.rb b/app/services/ci/pipeline_trigger_service.rb index 0e99f142492..37b9b4c362c 100644 --- a/app/services/ci/pipeline_trigger_service.rb +++ b/app/services/ci/pipeline_trigger_service.rb @@ -38,11 +38,34 @@ module Ci end def create_pipeline_from_job(job) - # overridden in EE + # this check is to not leak the presence of the project if user cannot read it + return unless can?(job.user, :read_project, project) + + return error("400 Job has to be running", 400) unless job.running? + + pipeline = Ci::CreatePipelineService.new(project, job.user, ref: params[:ref]) + .execute(:pipeline, ignore_skip_ci: true) do |pipeline| + source = job.sourced_pipelines.build( + source_pipeline: job.pipeline, + source_project: job.project, + pipeline: pipeline, + project: project) + + pipeline.source_pipeline = source + pipeline.variables.build(variables) + end + + if pipeline.persisted? + success(pipeline: pipeline) + else + error(pipeline.errors.messages, 400) + end end def job_from_token - # overridden in EE + strong_memoize(:job) do + Ci::Build.find_by_token(params[:token].to_s) + end end def variables @@ -52,5 +75,3 @@ module Ci end end end - -Ci::PipelineTriggerService.prepend_if_ee('EE::Ci::PipelineTriggerService') diff --git a/app/services/groups/transfer_service.rb b/app/services/groups/transfer_service.rb index fe7e07ef9f0..6902b7bd529 100644 --- a/app/services/groups/transfer_service.rb +++ b/app/services/groups/transfer_service.rb @@ -7,7 +7,8 @@ module Groups namespace_with_same_path: s_('TransferGroup|The parent group already has a subgroup with the same path.'), group_is_already_root: s_('TransferGroup|Group is already a root group.'), same_parent_as_current: s_('TransferGroup|Group is already associated to the parent group.'), - invalid_policies: s_("TransferGroup|You don't have enough permissions.") + invalid_policies: s_("TransferGroup|You don't have enough permissions."), + group_contains_images: s_('TransferGroup|Cannot update the path because there are projects under this group that contain Docker images in their Container Registry. Please remove the images from your projects first and try again.') }.freeze TransferError = Class.new(StandardError) @@ -46,6 +47,7 @@ module Groups raise_transfer_error(:same_parent_as_current) if same_parent? raise_transfer_error(:invalid_policies) unless valid_policies? raise_transfer_error(:namespace_with_same_path) if namespace_with_same_path? + raise_transfer_error(:group_contains_images) if group_projects_contain_registry_images? end def group_is_already_root? @@ -72,6 +74,10 @@ module Groups end # rubocop: enable CodeReuse/ActiveRecord + def group_projects_contain_registry_images? + @group.has_container_repositories? + end + def update_group_attributes if @new_parent_group && @new_parent_group.visibility_level < @group.visibility_level update_children_and_projects_visibility diff --git a/app/services/groups/update_service.rb b/app/services/groups/update_service.rb index 534de601e20..be7502a193e 100644 --- a/app/services/groups/update_service.rb +++ b/app/services/groups/update_service.rb @@ -8,6 +8,11 @@ module Groups reject_parent_id! remove_unallowed_params + if renaming_group_with_container_registry_images? + group.errors.add(:base, container_images_error) + return false + end + return false unless valid_visibility_level_change?(group, params[:visibility_level]) return false unless valid_share_with_group_lock_change? @@ -35,6 +40,17 @@ module Groups # overridden in EE end + def renaming_group_with_container_registry_images? + new_path = params[:path] + + new_path && new_path != group.path && + group.has_container_repositories? + end + + def container_images_error + s_("GroupSettings|Cannot update the path because there are projects under this group that contain Docker images in their Container Registry. Please remove the images from your projects first and try again.") + end + def after_update if group.previous_changes.include?(:visibility_level) && group.private? # don't enqueue immediately to prevent todos removal in case of a mistake diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml index a55b7fc530a..c8b2adcf084 100644 --- a/app/views/shared/issuable/_sidebar.html.haml +++ b/app/views/shared/issuable/_sidebar.html.haml @@ -5,174 +5,178 @@ - signed_in = !!issuable_sidebar.dig(:current_user, :id) - can_edit_issuable = issuable_sidebar.dig(:current_user, :can_edit) -%aside.right-sidebar.js-right-sidebar.js-issuable-sidebar{ data: { signed: { in: signed_in } }, class: sidebar_gutter_collapsed_class, 'aria-live' => 'polite' } - .issuable-sidebar - .block.issuable-sidebar-header - - if signed_in - %span.issuable-header-text.hide-collapsed.float-left - = _('To Do') - %a.gutter-toggle.float-right.js-sidebar-toggle.has-tooltip{ role: "button", href: "#", "aria-label" => "Toggle sidebar", title: sidebar_gutter_tooltip_text, data: { container: 'body', placement: 'left', boundary: 'viewport' } } - = sidebar_gutter_toggle_icon - - if signed_in - = render "shared/issuable/sidebar_todo", issuable_sidebar: issuable_sidebar - - = form_for issuable_type, url: issuable_sidebar[:issuable_json_path], remote: true, html: { class: 'issuable-context-form inline-update js-issuable-update' } do |f| - - if signed_in - .block.todo.hide-expanded - = render "shared/issuable/sidebar_todo", issuable_sidebar: issuable_sidebar, is_collapsed: true - .block.assignee.qa-assignee-block - = render "shared/issuable/sidebar_assignees", issuable_sidebar: issuable_sidebar, assignees: assignees - - = render_if_exists 'shared/issuable/sidebar_item_epic', issuable_sidebar: issuable_sidebar - - - milestone = issuable_sidebar[:milestone] || {} - .block.milestone - .sidebar-collapsed-icon.has-tooltip{ title: sidebar_milestone_tooltip_label(milestone), data: { container: 'body', html: 'true', placement: 'left', boundary: 'viewport' } } - = icon('clock-o', 'aria-hidden': 'true') - %span.milestone-title.collapse-truncated-title - - if milestone.present? - = milestone[:title] - - else - = _('None') - .title.hide-collapsed - = _('Milestone') - = icon('spinner spin', class: 'hidden block-loading', 'aria-hidden': 'true') - - if can_edit_issuable - = link_to _('Edit'), '#', class: 'js-sidebar-dropdown-toggle edit-link float-right', data: { track_label: "right_sidebar", track_property: "milestone", track_event: "click_edit_button", track_value: "" } - .value.hide-collapsed - - if milestone.present? - = link_to milestone[:title], milestone[:web_url], class: "bold has-tooltip", title: sidebar_milestone_remaining_days(milestone), data: { container: "body", html: 'true', boundary: 'viewport', qa_selector: 'milestone_link' } - - else - %span.no-value - = _('None') - - .selectbox.hide-collapsed - = f.hidden_field 'milestone_id', value: milestone[:id], id: nil - = dropdown_tag('Milestone', options: { title: _('Assign milestone'), toggle_class: 'js-milestone-select js-extra-options', filter: true, dropdown_class: 'dropdown-menu-selectable', placeholder: _('Search milestones'), data: { show_no: true, field_name: "#{issuable_type}[milestone_id]", project_id: issuable_sidebar[:project_id], issuable_id: issuable_sidebar[:id], milestones: issuable_sidebar[:project_milestones_path], ability_name: issuable_type, issue_update: issuable_sidebar[:issuable_json_path], use_id: true, default_no: true, selected: milestone[:title], null_default: true, display: 'static' }}) - - #issuable-time-tracker.block - // Fallback while content is loading - .title.hide-collapsed - = _('Time tracking') - = icon('spinner spin', 'aria-hidden': 'true') - - - if issuable_sidebar.has_key?(:due_date) - .block.due_date - .sidebar-collapsed-icon.has-tooltip{ data: { placement: 'left', container: 'body', html: 'true', boundary: 'viewport' }, title: sidebar_due_date_tooltip_label(issuable_sidebar[:due_date]) } - = icon('calendar', 'aria-hidden': 'true') - %span.js-due-date-sidebar-value - = issuable_sidebar[:due_date].try(:to_s, :medium) || 'None' +- if Feature.enabled?(:vue_issuable_sidebar, @project.group) + %aside#js-vue-issuable-sidebar{ data: { signed_in: signed_in, + sidebar_status_class: sidebar_gutter_collapsed_class } } +- else + %aside.right-sidebar.js-right-sidebar.js-issuable-sidebar{ data: { signed: { in: signed_in } }, class: sidebar_gutter_collapsed_class, 'aria-live' => 'polite' } + .issuable-sidebar + .block.issuable-sidebar-header + - if signed_in + %span.issuable-header-text.hide-collapsed.float-left + = _('To Do') + %a.gutter-toggle.float-right.js-sidebar-toggle.has-tooltip{ role: "button", href: "#", "aria-label" => "Toggle sidebar", title: sidebar_gutter_tooltip_text, data: { container: 'body', placement: 'left', boundary: 'viewport' } } + = sidebar_gutter_toggle_icon + - if signed_in + = render "shared/issuable/sidebar_todo", issuable_sidebar: issuable_sidebar + + = form_for issuable_type, url: issuable_sidebar[:issuable_json_path], remote: true, html: { class: 'issuable-context-form inline-update js-issuable-update' } do |f| + - if signed_in + .block.todo.hide-expanded + = render "shared/issuable/sidebar_todo", issuable_sidebar: issuable_sidebar, is_collapsed: true + .block.assignee.qa-assignee-block + = render "shared/issuable/sidebar_assignees", issuable_sidebar: issuable_sidebar, assignees: assignees + + = render_if_exists 'shared/issuable/sidebar_item_epic', issuable_sidebar: issuable_sidebar + + - milestone = issuable_sidebar[:milestone] || {} + .block.milestone + .sidebar-collapsed-icon.has-tooltip{ title: sidebar_milestone_tooltip_label(milestone), data: { container: 'body', html: 'true', placement: 'left', boundary: 'viewport' } } + = icon('clock-o', 'aria-hidden': 'true') + %span.milestone-title.collapse-truncated-title + - if milestone.present? + = milestone[:title] + - else + = _('None') .title.hide-collapsed - = _('Due date') + = _('Milestone') = icon('spinner spin', class: 'hidden block-loading', 'aria-hidden': 'true') - if can_edit_issuable - = link_to _('Edit'), '#', class: 'js-sidebar-dropdown-toggle edit-link float-right', data: { track_label: "right_sidebar", track_property: "due_date", track_event: "click_edit_button", track_value: "" } + = link_to _('Edit'), '#', class: 'js-sidebar-dropdown-toggle edit-link float-right', data: { track_label: "right_sidebar", track_property: "milestone", track_event: "click_edit_button", track_value: "" } .value.hide-collapsed - %span.value-content - - if issuable_sidebar[:due_date] - %span.bold= issuable_sidebar[:due_date].to_s(:medium) - - else - %span.no-value - = _('None') + - if milestone.present? + = link_to milestone[:title], milestone[:web_url], class: "bold has-tooltip", title: sidebar_milestone_remaining_days(milestone), data: { container: "body", html: 'true', boundary: 'viewport', qa_selector: 'milestone_link' } + - else + %span.no-value + = _('None') + + .selectbox.hide-collapsed + = f.hidden_field 'milestone_id', value: milestone[:id], id: nil + = dropdown_tag('Milestone', options: { title: _('Assign milestone'), toggle_class: 'js-milestone-select js-extra-options', filter: true, dropdown_class: 'dropdown-menu-selectable', placeholder: _('Search milestones'), data: { show_no: true, field_name: "#{issuable_type}[milestone_id]", project_id: issuable_sidebar[:project_id], issuable_id: issuable_sidebar[:id], milestones: issuable_sidebar[:project_milestones_path], ability_name: issuable_type, issue_update: issuable_sidebar[:issuable_json_path], use_id: true, default_no: true, selected: milestone[:title], null_default: true, display: 'static' }}) + + #issuable-time-tracker.block + // Fallback while content is loading + .title.hide-collapsed + = _('Time tracking') + = icon('spinner spin', 'aria-hidden': 'true') + + - if issuable_sidebar.has_key?(:due_date) + .block.due_date + .sidebar-collapsed-icon.has-tooltip{ data: { placement: 'left', container: 'body', html: 'true', boundary: 'viewport' }, title: sidebar_due_date_tooltip_label(issuable_sidebar[:due_date]) } + = icon('calendar', 'aria-hidden': 'true') + %span.js-due-date-sidebar-value + = issuable_sidebar[:due_date].try(:to_s, :medium) || 'None' + .title.hide-collapsed + = _('Due date') + = icon('spinner spin', class: 'hidden block-loading', 'aria-hidden': 'true') + - if can_edit_issuable + = link_to _('Edit'), '#', class: 'js-sidebar-dropdown-toggle edit-link float-right', data: { track_label: "right_sidebar", track_property: "due_date", track_event: "click_edit_button", track_value: "" } + .value.hide-collapsed + %span.value-content + - if issuable_sidebar[:due_date] + %span.bold= issuable_sidebar[:due_date].to_s(:medium) + - else + %span.no-value + = _('None') + - if can_edit_issuable + %span.no-value.js-remove-due-date-holder{ class: ("hidden" if issuable_sidebar[:due_date].nil?) } + \- + %a.js-remove-due-date{ href: "#", role: "button" } + = _('remove due date') - if can_edit_issuable - %span.no-value.js-remove-due-date-holder{ class: ("hidden" if issuable_sidebar[:due_date].nil?) } - \- - %a.js-remove-due-date{ href: "#", role: "button" } - = _('remove due date') - - if can_edit_issuable - .selectbox.hide-collapsed - = f.hidden_field :due_date, value: issuable_sidebar[:due_date].try(:strftime, 'yy-mm-dd') - .dropdown - %button.dropdown-menu-toggle.js-due-date-select{ type: 'button', data: { toggle: 'dropdown', field_name: "#{issuable_type}[due_date]", ability_name: issuable_type, issue_update: issuable_sidebar[:issuable_json_path], display: 'static' } } - %span.dropdown-toggle-text - = _('Due date') - = icon('chevron-down', 'aria-hidden': 'true') - .dropdown-menu.dropdown-menu-due-date - = dropdown_title(_('Due date')) - = dropdown_content do - .js-due-date-calendar - - - selected_labels = issuable_sidebar[:labels] - .block.labels - .sidebar-collapsed-icon.js-sidebar-labels-tooltip{ title: issuable_labels_tooltip(selected_labels), data: { placement: "left", container: "body", boundary: 'viewport' } } - = icon('tags', 'aria-hidden': 'true') - %span - = selected_labels.size - .title.hide-collapsed - = _('Labels') - = icon('spinner spin', class: 'hidden block-loading', 'aria-hidden': 'true') - - if can_edit_issuable - = link_to _('Edit'), '#', class: 'js-sidebar-dropdown-toggle edit-link qa-edit-link-labels float-right', data: { track_label: "right_sidebar", track_property: "labels", track_event: "click_edit_button", track_value: "" } - .value.issuable-show-labels.dont-hide.hide-collapsed.qa-labels-block{ class: ("has-labels" if selected_labels.any?) } - - if selected_labels.any? - - selected_labels.each do |label_hash| - = render_label(label_from_hash(label_hash).present(issuable_subject: nil), link: sidebar_label_filter_path(issuable_sidebar[:project_issuables_path], label_hash[:title])) - - else - %span.no-value - = _('None') - .selectbox.hide-collapsed - - selected_labels.each do |label| - = hidden_field_tag "#{issuable_type}[label_names][]", label[:id], id: nil - .dropdown - %button.dropdown-menu-toggle.js-label-select.js-multiselect.js-label-sidebar-dropdown{ type: "button", data: sidebar_label_dropdown_data(issuable_type, issuable_sidebar) } - %span.dropdown-toggle-text{ class: ("is-default" if selected_labels.empty?) } - = multi_label_name(selected_labels, "Labels") - = icon('chevron-down', 'aria-hidden': 'true') - .dropdown-menu.dropdown-select.dropdown-menu-paging.qa-dropdown-menu-labels.dropdown-menu-labels.dropdown-menu-selectable.dropdown-extended-height - = render partial: "shared/issuable/label_page_default" - - if issuable_sidebar.dig(:current_user, :can_admin_label) - = render partial: "shared/issuable/label_page_create" - - = render_if_exists 'shared/issuable/sidebar_weight', issuable_sidebar: issuable_sidebar - - - if issuable_sidebar.has_key?(:confidential) + .selectbox.hide-collapsed + = f.hidden_field :due_date, value: issuable_sidebar[:due_date].try(:strftime, 'yy-mm-dd') + .dropdown + %button.dropdown-menu-toggle.js-due-date-select{ type: 'button', data: { toggle: 'dropdown', field_name: "#{issuable_type}[due_date]", ability_name: issuable_type, issue_update: issuable_sidebar[:issuable_json_path], display: 'static' } } + %span.dropdown-toggle-text + = _('Due date') + = icon('chevron-down', 'aria-hidden': 'true') + .dropdown-menu.dropdown-menu-due-date + = dropdown_title(_('Due date')) + = dropdown_content do + .js-due-date-calendar + + - selected_labels = issuable_sidebar[:labels] + .block.labels + .sidebar-collapsed-icon.js-sidebar-labels-tooltip{ title: issuable_labels_tooltip(selected_labels), data: { placement: "left", container: "body", boundary: 'viewport' } } + = icon('tags', 'aria-hidden': 'true') + %span + = selected_labels.size + .title.hide-collapsed + = _('Labels') + = icon('spinner spin', class: 'hidden block-loading', 'aria-hidden': 'true') + - if can_edit_issuable + = link_to _('Edit'), '#', class: 'js-sidebar-dropdown-toggle edit-link qa-edit-link-labels float-right', data: { track_label: "right_sidebar", track_property: "labels", track_event: "click_edit_button", track_value: "" } + .value.issuable-show-labels.dont-hide.hide-collapsed.qa-labels-block{ class: ("has-labels" if selected_labels.any?) } + - if selected_labels.any? + - selected_labels.each do |label_hash| + = render_label(label_from_hash(label_hash).present(issuable_subject: nil), link: sidebar_label_filter_path(issuable_sidebar[:project_issuables_path], label_hash[:title])) + - else + %span.no-value + = _('None') + .selectbox.hide-collapsed + - selected_labels.each do |label| + = hidden_field_tag "#{issuable_type}[label_names][]", label[:id], id: nil + .dropdown + %button.dropdown-menu-toggle.js-label-select.js-multiselect.js-label-sidebar-dropdown{ type: "button", data: sidebar_label_dropdown_data(issuable_type, issuable_sidebar) } + %span.dropdown-toggle-text{ class: ("is-default" if selected_labels.empty?) } + = multi_label_name(selected_labels, "Labels") + = icon('chevron-down', 'aria-hidden': 'true') + .dropdown-menu.dropdown-select.dropdown-menu-paging.qa-dropdown-menu-labels.dropdown-menu-labels.dropdown-menu-selectable.dropdown-extended-height + = render partial: "shared/issuable/label_page_default" + - if issuable_sidebar.dig(:current_user, :can_admin_label) + = render partial: "shared/issuable/label_page_create" + + = render_if_exists 'shared/issuable/sidebar_weight', issuable_sidebar: issuable_sidebar + + - if issuable_sidebar.has_key?(:confidential) + -# haml-lint:disable InlineJavaScript + %script#js-confidential-issue-data{ type: "application/json" }= { is_confidential: issuable_sidebar[:confidential], is_editable: can_edit_issuable }.to_json.html_safe + #js-confidential-entry-point + -# haml-lint:disable InlineJavaScript - %script#js-confidential-issue-data{ type: "application/json" }= { is_confidential: issuable_sidebar[:confidential], is_editable: can_edit_issuable }.to_json.html_safe - #js-confidential-entry-point + %script#js-lock-issue-data{ type: "application/json" }= { is_locked: !!issuable_sidebar[:discussion_locked], is_editable: can_edit_issuable }.to_json.html_safe + #js-lock-entry-point + + .js-sidebar-participants-entry-point + + - if signed_in + - if issuable_sidebar[:project_emails_disabled] + .block.js-emails-disabled + .sidebar-collapsed-icon.has-tooltip{ title: notification_description(:owner_disabled), data: { placement: "left", container: "body", boundary: 'viewport' } } + = notification_setting_icon + .hide-collapsed= notification_description(:owner_disabled) + - else + .js-sidebar-subscriptions-entry-point + + - project_ref = issuable_sidebar[:reference] + .block.project-reference + .sidebar-collapsed-icon.dont-change-state + = clipboard_button(text: project_ref, title: _('Copy reference'), placement: "left", boundary: 'viewport') + .cross-project-reference.hide-collapsed + %span + = _('Reference:') + %cite{ title: project_ref } + = project_ref + = clipboard_button(text: project_ref, title: _('Copy reference'), placement: "left", boundary: 'viewport') + + - if issuable_sidebar.dig(:current_user, :can_move) + .block.js-sidebar-move-issue-block + .sidebar-collapsed-icon{ data: { toggle: 'tooltip', placement: 'left', container: 'body', boundary: 'viewport' }, title: _('Move issue') } + = custom_icon('icon_arrow_right') + .dropdown.sidebar-move-issue-dropdown.hide-collapsed + %button.btn.btn-default.btn-block.js-sidebar-dropdown-toggle.js-move-issue{ type: 'button', + data: { toggle: 'dropdown', display: 'static', track_label: "right_sidebar", track_property: "move_issue", track_event: "click_button", track_value: "" } } + = _('Move issue') + .dropdown-menu.dropdown-menu-selectable.dropdown-extended-height + = dropdown_title(_('Move issue')) + = dropdown_filter(_('Search project'), search_id: 'sidebar-move-issue-dropdown-search') + = dropdown_content + = dropdown_loading + = dropdown_footer add_content_class: true do + %button.btn.btn-success.sidebar-move-issue-confirmation-button.js-move-issue-confirmation-button{ type: 'button', disabled: true } + = _('Move') + = icon('spinner spin', class: 'sidebar-move-issue-confirmation-loading-icon') -# haml-lint:disable InlineJavaScript - %script#js-lock-issue-data{ type: "application/json" }= { is_locked: !!issuable_sidebar[:discussion_locked], is_editable: can_edit_issuable }.to_json.html_safe - #js-lock-entry-point - - .js-sidebar-participants-entry-point - - - if signed_in - - if issuable_sidebar[:project_emails_disabled] - .block.js-emails-disabled - .sidebar-collapsed-icon.has-tooltip{ title: notification_description(:owner_disabled), data: { placement: "left", container: "body", boundary: 'viewport' } } - = notification_setting_icon - .hide-collapsed= notification_description(:owner_disabled) - - else - .js-sidebar-subscriptions-entry-point - - - project_ref = issuable_sidebar[:reference] - .block.project-reference - .sidebar-collapsed-icon.dont-change-state - = clipboard_button(text: project_ref, title: _('Copy reference'), placement: "left", boundary: 'viewport') - .cross-project-reference.hide-collapsed - %span - = _('Reference:') - %cite{ title: project_ref } - = project_ref - = clipboard_button(text: project_ref, title: _('Copy reference'), placement: "left", boundary: 'viewport') - - - if issuable_sidebar.dig(:current_user, :can_move) - .block.js-sidebar-move-issue-block - .sidebar-collapsed-icon{ data: { toggle: 'tooltip', placement: 'left', container: 'body', boundary: 'viewport' }, title: _('Move issue') } - = custom_icon('icon_arrow_right') - .dropdown.sidebar-move-issue-dropdown.hide-collapsed - %button.btn.btn-default.btn-block.js-sidebar-dropdown-toggle.js-move-issue{ type: 'button', - data: { toggle: 'dropdown', display: 'static', track_label: "right_sidebar", track_property: "move_issue", track_event: "click_button", track_value: "" } } - = _('Move issue') - .dropdown-menu.dropdown-menu-selectable.dropdown-extended-height - = dropdown_title(_('Move issue')) - = dropdown_filter(_('Search project'), search_id: 'sidebar-move-issue-dropdown-search') - = dropdown_content - = dropdown_loading - = dropdown_footer add_content_class: true do - %button.btn.btn-success.sidebar-move-issue-confirmation-button.js-move-issue-confirmation-button{ type: 'button', disabled: true } - = _('Move') - = icon('spinner spin', class: 'sidebar-move-issue-confirmation-loading-icon') - - -# haml-lint:disable InlineJavaScript - %script.js-sidebar-options{ type: "application/json" }= issuable_sidebar_options(issuable_sidebar).to_json.html_safe + %script.js-sidebar-options{ type: "application/json" }= issuable_sidebar_options(issuable_sidebar).to_json.html_safe |