diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2023-10-24 21:11:45 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2023-10-24 21:11:45 +0300 |
commit | 4bb797f25563205cf495f4dd5366e037e88831ab (patch) | |
tree | a345ddbd0e2464067323d3c6fd34960607ef4f44 /app | |
parent | 40a4f37126bb1a1dd6b6f4b3c0ebb414a3e3908a (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
76 files changed, 642 insertions, 289 deletions
diff --git a/app/assets/javascripts/batch_comments/components/submit_dropdown.vue b/app/assets/javascripts/batch_comments/components/submit_dropdown.vue index 0527b56b464..4b9fe01e997 100644 --- a/app/assets/javascripts/batch_comments/components/submit_dropdown.vue +++ b/app/assets/javascripts/batch_comments/components/submit_dropdown.vue @@ -6,11 +6,29 @@ import { __ } from '~/locale'; import { createAlert } from '~/alert'; import MarkdownEditor from '~/vue_shared/components/markdown/markdown_editor.vue'; import { scrollToElement } from '~/lib/utils/common_utils'; +import { fetchPolicies } from '~/lib/graphql'; import { CLEAR_AUTOSAVE_ENTRY_EVENT } from '~/vue_shared/constants'; import markdownEditorEventHub from '~/vue_shared/components/markdown/eventhub'; import { trackSavedUsingEditor } from '~/vue_shared/components/markdown/tracking'; +import userCanApproveQuery from '../queries/can_approve.query.graphql'; export default { + apollo: { + userPermissions: { + fetchPolicy: fetchPolicies.NETWORK_ONLY, + query: userCanApproveQuery, + variables() { + return { + projectPath: this.projectPath.replace(/^\//, ''), + iid: `${this.getNoteableData.iid}`, + }; + }, + update: (data) => data.project?.mergeRequest?.userPermissions, + skip() { + return !this.dropdownVisible; + }, + }, + }, components: { GlDisclosureDropdown, GlButton, @@ -28,6 +46,7 @@ export default { data() { return { isSubmitting: false, + dropdownVisible: false, noteData: { noteable_type: '', noteable_id: '', @@ -42,11 +61,13 @@ export default { 'aria-label': __('Comment'), 'data-testid': 'comment-textarea', }, + userPermissions: {}, }; }, computed: { ...mapGetters(['getNotesData', 'getNoteableData', 'noteableType', 'getCurrentUserLastNote']), ...mapState('batchComments', ['shouldAnimateReviewButton']), + ...mapState('diffs', ['projectPath']), autocompleteDataSources() { return gl.GfmAutoComplete?.dataSources; }, @@ -60,6 +81,17 @@ export default { this.repositionDropdown(); }); }, + dropdownVisible(val) { + if (!val) { + this.userPermissions = {}; + } + }, + userPermissions: { + handler() { + this.repositionDropdown(); + }, + deep: true, + }, }, mounted() { this.noteData.noteable_type = this.noteableType; @@ -112,6 +144,9 @@ export default { preventDefault(); } }, + setDropdownVisible(val) { + this.dropdownVisible = val; + }, }, restrictedToolbarItems: ['full-screen'], }; @@ -126,6 +161,8 @@ export default { data-testid="submit-review-dropdown" fluid-width @beforeClose="onBeforeClose" + @shown="setDropdownVisible(true)" + @hidden="setDropdownVisible(false)" > <template #toggle> <gl-button variant="info" category="primary"> @@ -171,7 +208,7 @@ export default { @keydown.ctrl.enter="submitReview" /> </div> - <template v-if="getNoteableData.current_user.can_approve"> + <template v-if="userPermissions.canApprove"> <gl-form-checkbox v-model="noteData.approve" data-testid="approve_merge_request" diff --git a/app/assets/javascripts/batch_comments/queries/can_approve.query.graphql b/app/assets/javascripts/batch_comments/queries/can_approve.query.graphql new file mode 100644 index 00000000000..f0c9ef7b3c8 --- /dev/null +++ b/app/assets/javascripts/batch_comments/queries/can_approve.query.graphql @@ -0,0 +1,11 @@ +query userCanApprove($projectPath: ID!, $iid: String!) { + project(fullPath: $projectPath) { + id + mergeRequest(iid: $iid) { + id + userPermissions { + canApprove + } + } + } +} diff --git a/app/assets/javascripts/ci/common/private/job_action_component.vue b/app/assets/javascripts/ci/common/private/job_action_component.vue index b0fa724d450..c266e061513 100644 --- a/app/assets/javascripts/ci/common/private/job_action_component.vue +++ b/app/assets/javascripts/ci/common/private/job_action_component.vue @@ -119,6 +119,7 @@ export default { ref="button" :class="cssClass" :disabled="isDisabled" + size="small" class="js-ci-action gl-ci-action-icon-container ci-action-icon-container ci-action-icon-wrapper gl-display-flex gl-align-items-center gl-justify-content-center" data-testid="ci-action-button" @click.stop="onClickAction" @@ -129,8 +130,17 @@ export default { class="gl-display-flex gl-align-items-center gl-justify-content-center gl-h-full" data-testid="ci-action-icon-tooltip-wrapper" > - <gl-loading-icon v-if="isLoading" size="sm" class="js-action-icon-loading" /> - <gl-icon v-else :name="actionIcon" class="gl-mr-0!" :aria-label="actionIcon" /> + <gl-loading-icon + v-if="isLoading" + size="sm" + class="gl-button-icon gl-m-2 js-action-icon-loading" + /> + <gl-icon + v-else + :name="actionIcon" + class="gl-button-icon gl-p-1 gl-mr-0!" + :aria-label="actionIcon" + /> </div> </gl-button> </template> diff --git a/app/assets/javascripts/ci/common/private/job_links_layer.vue b/app/assets/javascripts/ci/common/private/job_links_layer.vue index 59260ca3f81..9b3647e9c55 100644 --- a/app/assets/javascripts/ci/common/private/job_links_layer.vue +++ b/app/assets/javascripts/ci/common/private/job_links_layer.vue @@ -1,5 +1,6 @@ <script> import { memoize } from 'lodash'; +import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import { reportToSentry } from '~/ci/utils'; import { parseData } from '~/ci/pipeline_details/utils/parsing_utils'; import LinksInner from '~/ci/pipeline_details/graph/components/links_inner.vue'; @@ -16,6 +17,7 @@ export default { components: { LinksInner, }, + mixins: [glFeatureFlagMixin()], props: { containerMeasurements: { type: Object, @@ -50,6 +52,9 @@ export default { showLinkedLayers() { return this.showLinks && !this.containerZero; }, + isNewPipelineGraph() { + return this.glFeatures.newPipelineGraph; + }, }, errorCaptured(err, _vm, info) { reportToSentry(this.$options.name, `error: ${err}, info: ${info}`); @@ -68,7 +73,10 @@ export default { <slot></slot> </links-inner> <div v-else> - <div class="gl-display-flex gl-relative"> + <div + class="gl-display-flex gl-relative" + :class="{ 'gl-flex-wrap gl-sm-flex-nowrap': isNewPipelineGraph }" + > <slot></slot> </div> </div> diff --git a/app/assets/javascripts/ci/pipeline_details/graph/components/graph_component.vue b/app/assets/javascripts/ci/pipeline_details/graph/components/graph_component.vue index f098d790736..fce7aabf0cf 100644 --- a/app/assets/javascripts/ci/pipeline_details/graph/components/graph_component.vue +++ b/app/assets/javascripts/ci/pipeline_details/graph/components/graph_component.vue @@ -4,6 +4,7 @@ import { generateColumnsFromLayersListMemoized, keepLatestDownstreamPipelines, } from '~/ci/pipeline_details/utils/parsing_utils'; +import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import LinksLayer from '../../../common/private/job_links_layer.vue'; import { DOWNSTREAM, MAIN, UPSTREAM, ONE_COL_WIDTH, STAGE_VIEW } from '../constants'; import { validateConfigPaths } from '../utils'; @@ -19,6 +20,7 @@ export default { LinkedPipelinesColumn, StageColumnComponent, }, + mixins: [glFeatureFlagMixin()], props: { configPaths: { type: Object, @@ -132,6 +134,9 @@ export default { upstreamPipelines() { return this.hasUpstreamPipelines ? this.pipeline.upstream : []; }, + isNewPipelineGraph() { + return this.glFeatures.newPipelineGraph; + }, }, errorCaptured(err, _vm, info) { reportToSentry(this.$options.name, `error: ${err}, info: ${info}`); @@ -178,10 +183,15 @@ export default { <div class="js-pipeline-graph"> <div ref="mainPipelineContainer" - class="gl-display-flex gl-position-relative gl-bg-gray-10 gl-white-space-nowrap" + class="pipeline-graph gl-display-flex gl-position-relative gl-white-space-nowrap gl-rounded-lg" :class="{ - 'gl-pipeline-min-h gl-py-5 gl-overflow-auto': !isLinkedPipeline, + 'gl-bg-gray-10': !isNewPipelineGraph, + 'gl-pipeline-min-h gl-py-5 gl-overflow-auto': !isNewPipelineGraph && !isLinkedPipeline, + 'pipeline-graph-container gl-bg-gray-10 gl-pipeline-min-h gl-align-items-flex-start gl-pt-3 gl-pb-8 gl-mt-3 gl-overflow-auto': + isNewPipelineGraph && !isLinkedPipeline, + 'gl-bg-gray-50 gl-sm-ml-5': isNewPipelineGraph && isLinkedPipeline, }" + data-testid="pipeline-container" > <linked-graph-wrapper> <template #upstream> @@ -199,7 +209,7 @@ export default { /> </template> <template #main> - <div :id="containerId" :ref="containerId"> + <div :id="containerId" :ref="containerId" class="pipeline-links-container"> <links-layer :pipeline-data="layout" :pipeline-id="pipeline.id" @@ -238,7 +248,8 @@ export default { <template #downstream> <linked-pipelines-column v-if="showDownstreamPipelines" - class="gl-mr-6" + class="gl-mr-5" + :class="{ 'gl-sm-ml-3': isNewPipelineGraph }" :config-paths="configPaths" :linked-pipelines="downstreamPipelines" :column-title="__('Downstream')" diff --git a/app/assets/javascripts/ci/pipeline_details/graph/components/graph_view_selector.vue b/app/assets/javascripts/ci/pipeline_details/graph/components/graph_view_selector.vue index fb7dcb300f1..114b224fbe7 100644 --- a/app/assets/javascripts/ci/pipeline_details/graph/components/graph_view_selector.vue +++ b/app/assets/javascripts/ci/pipeline_details/graph/components/graph_view_selector.vue @@ -1,11 +1,11 @@ <script> import { GlAlert, GlButton, GlButtonGroup, GlLoadingIcon, GlToggle } from '@gitlab/ui'; +import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import { __, s__ } from '~/locale'; import { STAGE_VIEW, LAYER_VIEW } from '../constants'; export default { name: 'GraphViewSelector', - components: { GlAlert, GlButton, @@ -13,7 +13,7 @@ export default { GlLoadingIcon, GlToggle, }, - + mixins: [glFeatureFlagMixin()], props: { showLinks: { type: Boolean, @@ -77,6 +77,9 @@ export default { }; }); }, + isNewPipelineGraph() { + return this.glFeatures.newPipelineGraph; + }, }, watch: { /* @@ -138,7 +141,13 @@ export default { <template> <div> - <div class="gl-relative gl-display-flex gl-align-items-center gl-w-max-content gl-my-4"> + <div + class="gl-relative gl-display-flex gl-align-items-center gl-my-4" + :class="{ + 'gl-w-max-content': !isNewPipelineGraph, + 'gl-flex-wrap gl-sm-flex-nowrap': isNewPipelineGraph, + }" + > <gl-loading-icon v-if="isSwitcherLoading" data-testid="switcher-loading-state" @@ -161,7 +170,10 @@ export default { <gl-toggle v-model="showLinksActive" data-testid="show-links-toggle" - class="gl-mx-4" + :class="{ + 'gl-mx-4': !isNewPipelineGraph, + 'gl-sm-ml-4 gl-mt-4 gl-sm-mt-0': isNewPipelineGraph, + }" :label="$options.i18n.linksLabelText" :is-loading="isToggleLoading" label-position="left" diff --git a/app/assets/javascripts/ci/pipeline_details/graph/components/linked_graph_wrapper.vue b/app/assets/javascripts/ci/pipeline_details/graph/components/linked_graph_wrapper.vue index fb2280d971a..9bd0ec6d793 100644 --- a/app/assets/javascripts/ci/pipeline_details/graph/components/linked_graph_wrapper.vue +++ b/app/assets/javascripts/ci/pipeline_details/graph/components/linked_graph_wrapper.vue @@ -1,5 +1,17 @@ +<script> +import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; + +export default { + mixins: [glFeatureFlagMixin()], + computed: { + isNewPipelineGraph() { + return this.glFeatures.newPipelineGraph; + }, + }, +}; +</script> <template> - <div class="gl-display-flex"> + <div class="gl-display-flex" :class="{ 'gl-flex-wrap gl-w-full': isNewPipelineGraph }"> <slot name="upstream"></slot> <slot name="main"></slot> <slot name="downstream"></slot> diff --git a/app/assets/javascripts/ci/pipeline_details/graph/components/linked_pipeline.vue b/app/assets/javascripts/ci/pipeline_details/graph/components/linked_pipeline.vue index 511eac79fb5..26521f87426 100644 --- a/app/assets/javascripts/ci/pipeline_details/graph/components/linked_pipeline.vue +++ b/app/assets/javascripts/ci/pipeline_details/graph/components/linked_pipeline.vue @@ -7,6 +7,7 @@ import { GlTooltip, GlTooltipDirective, } from '@gitlab/ui'; +import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import { TYPENAME_CI_PIPELINE } from '~/graphql_shared/constants'; import { convertToGraphQLId } from '~/graphql_shared/utils'; import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants'; @@ -29,6 +30,7 @@ export default { GlLoadingIcon, GlTooltip, }, + mixins: [glFeatureFlagMixin()], styles: { actionSizeClasses: ['gl-h-7 gl-w-7'], flatLeftBorder: ['gl-rounded-bottom-left-none!', 'gl-rounded-top-left-none!'], @@ -115,9 +117,6 @@ export default { downstreamTitle() { return this.childPipeline ? this.sourceJobName : this.pipeline.project.name; }, - flexDirection() { - return this.isUpstream ? 'gl-flex-direction-row-reverse' : 'gl-flex-direction-row'; - }, graphqlPipelineId() { return convertToGraphQLId(TYPENAME_CI_PIPELINE, this.pipeline.id); }, @@ -176,6 +175,9 @@ export default { return `${this.downstreamTitle} #${this.pipeline.id} - ${this.pipelineStatus.label} - ${this.sourceJobInfo}`; }, + isNewPipelineGraph() { + return this.glFeatures.newPipelineGraph; + }, }, errorCaptured(err, _vm, info) { reportToSentry('linked_pipeline', `error: ${err}, info: ${info}`); @@ -231,9 +233,15 @@ export default { <template> <div ref="linkedPipeline" - class="gl-h-full gl-display-flex! gl-px-2" - :class="flexDirection" + class="linked-pipeline-container gl-h-full gl-display-flex!" + :class="{ + 'gl-flex-direction-row-reverse': isUpstream, + 'gl-flex-direction-row': !isUpstream, + 'gl-px-2': !isNewPipelineGraph, + 'gl-w-full gl-sm-w-auto': isNewPipelineGraph, + }" data-testid="linked-pipeline-container" + :aria-expanded="expanded" @mouseover="onDownstreamHovered" @mouseleave="onDownstreamHoverLeave" > @@ -250,7 +258,7 @@ export default { /> <div v-else class="gl-pr-3"><gl-loading-icon size="sm" inline /></div> <div - class="gl-display-flex gl-downstream-pipeline-job-width gl-flex-direction-column gl-line-height-normal" + class="gl-display-flex gl-flex-direction-column gl-line-height-normal gl-downstream-pipeline-job-width" > <span class="gl-text-truncate" data-testid="downstream-title-content"> {{ downstreamTitle }} diff --git a/app/assets/javascripts/ci/pipeline_details/graph/components/linked_pipelines_column.vue b/app/assets/javascripts/ci/pipeline_details/graph/components/linked_pipelines_column.vue index 2de7e43c9b1..67918ea8d1a 100644 --- a/app/assets/javascripts/ci/pipeline_details/graph/components/linked_pipelines_column.vue +++ b/app/assets/javascripts/ci/pipeline_details/graph/components/linked_pipelines_column.vue @@ -1,4 +1,5 @@ <script> +import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import getPipelineDetails from 'shared_queries/pipelines/get_pipeline_details.query.graphql'; import { reportToSentry } from '~/ci/utils'; import { LOAD_FAILURE } from '../../constants'; @@ -18,6 +19,7 @@ export default { LinkedPipeline, PipelineGraph: () => import('./graph_component.vue'), }, + mixins: [glFeatureFlagMixin()], props: { columnTitle: { type: String, @@ -63,23 +65,30 @@ export default { 'gl-pipeline-job-width', 'gl-text-truncate', 'gl-line-height-36', - 'gl-pl-3', - 'gl-mb-5', ], minWidth: `${ONE_COL_WIDTH}px`, computed: { columnClass() { - const positionValues = { + const positionValuesOld = { right: 'gl-ml-6', left: 'gl-mx-6', }; + const positionValues = { + right: 'gl-ml-5', + left: 'gl-mx-4 gl-flex-basis-full', + }; + const usePositionValues = this.isNewPipelineGraph ? positionValues : positionValuesOld; - return `graph-position-${this.graphPosition} ${positionValues[this.graphPosition]}`; + return `graph-position-${this.graphPosition} ${usePositionValues[this.graphPosition]}`; }, computedTitleClasses() { const positionalClasses = this.isUpstream ? ['gl-w-full', 'gl-linked-pipeline-padding'] : []; - return [...this.$options.titleClasses, ...positionalClasses]; + return [ + ...this.$options.titleClasses, + !this.isNewPipelineGraph ?? ['gl-pl-3', 'gl-mb-5'], + ...positionalClasses, + ]; }, graphPosition() { return this.isUpstream ? 'left' : 'right'; @@ -93,6 +102,9 @@ export default { minWidth() { return this.isUpstream ? 0 : this.$options.minWidth; }, + isNewPipelineGraph() { + return this.glFeatures.newPipelineGraph; + }, }, methods: { getPipelineData(pipeline) { @@ -197,7 +209,7 @@ export default { </script> <template> - <div class="gl-display-flex"> + <div class="gl-display-flex" :class="{ 'gl-w-full': isNewPipelineGraph }"> <div :class="columnClass" class="linked-pipelines-column"> <div data-testid="linked-column-title" :class="computedTitleClasses"> {{ columnTitle }} @@ -206,8 +218,12 @@ export default { <li v-for="pipeline in linkedPipelines" :key="pipeline.id" - class="gl-display-flex gl-mb-3" - :class="{ 'gl-flex-direction-row-reverse': isUpstream }" + class="gl-display-flex" + :class="{ + 'gl-mb-3': !isNewPipelineGraph, + 'gl-flex-wrap gl-sm-flex-nowrap gl-mb-6': isNewPipelineGraph, + 'gl-flex-direction-row-reverse': !isNewPipelineGraph && isUpstream, + }" > <linked-pipeline class="gl-display-inline-block" @@ -224,12 +240,15 @@ export default { <div v-if="showContainer(pipeline.id)" :style="{ minWidth }" - class="gl-display-inline-block" + class="gl-display-inline-block pipeline-show-container" > <pipeline-graph v-if="isExpanded(pipeline.id)" :type="type" - class="gl-inline-block gl-mt-n2" + class="gl-inline-block" + :class="{ + 'gl-mt-n2': !isNewPipelineGraph, + }" :config-paths="configPaths" :pipeline="currentPipeline" :computed-pipeline-info="getPipelineLayers(pipeline.id)" diff --git a/app/assets/javascripts/ci/pipeline_details/graph/components/root_graph_layout.vue b/app/assets/javascripts/ci/pipeline_details/graph/components/root_graph_layout.vue index bcd7705669e..7c07591d0de 100644 --- a/app/assets/javascripts/ci/pipeline_details/graph/components/root_graph_layout.vue +++ b/app/assets/javascripts/ci/pipeline_details/graph/components/root_graph_layout.vue @@ -1,5 +1,12 @@ <script> +import { GlCard } from '@gitlab/ui'; +import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; + export default { + components: { + GlCard, + }, + mixins: [glFeatureFlagMixin()], props: { stageClasses: { type: String, @@ -12,18 +19,37 @@ export default { default: '', }, }, + computed: { + isNewPipelineGraph() { + return this.glFeatures.newPipelineGraph; + }, + }, }; </script> <template> <div> - <div class="gl-display-flex gl-align-items-center gl-w-full gl-mb-5" :class="stageClasses"> - <slot name="stages"> </slot> - </div> - <div - class="gl-display-flex gl-flex-direction-column gl-align-items-center gl-w-full" - :class="jobClasses" + <gl-card + v-if="isNewPipelineGraph" + class="gl-rounded-lg" + header-class="gl-rounded-lg gl-px-0 gl-py-0 gl-bg-white gl-border-b-0" + body-class="gl-pt-2 gl-pb-0 gl-px-2" > - <slot name="jobs"> </slot> - </div> + <template #header> + <slot name="stages"></slot> + </template> + + <slot name="jobs"></slot> + </gl-card> + <template v-else> + <div class="gl-display-flex gl-align-items-center gl-w-full" :class="stageClasses"> + <slot name="stages"> </slot> + </div> + <div + class="gl-display-flex gl-flex-direction-column gl-align-items-center gl-w-full" + :class="jobClasses" + > + <slot name="jobs"> </slot> + </div> + </template> </div> </template> diff --git a/app/assets/javascripts/ci/pipeline_details/graph/components/stage_column_component.vue b/app/assets/javascripts/ci/pipeline_details/graph/components/stage_column_component.vue index 6030adc96ad..e144b9aab0c 100644 --- a/app/assets/javascripts/ci/pipeline_details/graph/components/stage_column_component.vue +++ b/app/assets/javascripts/ci/pipeline_details/graph/components/stage_column_component.vue @@ -68,7 +68,7 @@ export default { required: true, }, }, - jobClasses: [ + legacyJobClasses: [ 'gl-p-3', 'gl-border-gray-100', 'gl-border-solid', @@ -82,18 +82,43 @@ export default { 'gl-hover-border-gray-200', 'gl-focus-border-gray-200', ], - titleClasses: [ + jobClasses: [ + 'gl-p-3', + 'gl-border-0', + 'gl-bg-transparent', + 'gl-rounded-base', + 'gl-hover-bg-gray-50', + 'gl-focus-bg-gray-50', + 'gl-hover-text-gray-900', + 'gl-focus-text-gray-900', + ], + legacyTitleClasses: [ 'gl-font-weight-bold', 'gl-pipeline-job-width', 'gl-text-truncate', 'gl-line-height-36', 'gl-pl-3', ], + titleClasses: [ + 'gl-font-weight-bold', + 'gl-pipeline-job-width', + 'gl-text-truncate', + 'gl-line-height-36', + 'gl-pl-4', + 'gl-mb-n2', + ], computed: { canUpdatePipeline() { return this.userPermissions.updatePipeline; }, columnSpacingClass() { + if (this.isNewPipelineGraph) { + const baseClasses = 'stage-column gl-relative gl-flex-basis-full'; + return this.isStageView + ? `${baseClasses} is-stage-view gl-m-5` + : `${baseClasses} gl-my-5 gl-mx-7`; + } + return this.isStageView ? 'gl-px-6' : 'gl-px-9'; }, hasAction() { @@ -102,6 +127,17 @@ export default { showStageName() { return !this.isStageView; }, + isNewPipelineGraph() { + return this.glFeatures.newPipelineGraph; + }, + jobClasses() { + return this.isNewPipelineGraph ? this.$options.jobClasses : this.$options.legacyJobClasses; + }, + titleClasses() { + return this.isNewPipelineGraph + ? this.$options.titleClasses + : this.$options.legacyTitleClasses; + }, }, errorCaptured(err, _vm, info) { reportToSentry('stage_column_component', `error: ${err}, info: ${info}`); @@ -135,12 +171,16 @@ export default { }; </script> <template> - <root-graph-layout :class="columnSpacingClass" data-testid="stage-column"> + <root-graph-layout + :class="columnSpacingClass" + class="stage-column gl-relative gl-flex-basis-full" + data-testid="stage-column" + > <template #stages> <div data-testid="stage-column-title" class="gl-display-flex gl-justify-content-space-between gl-relative" - :class="$options.titleClasses" + :class="titleClasses" > <span :title="name" class="gl-text-truncate gl-pr-3 gl-w-85p"> {{ name }} @@ -161,7 +201,11 @@ export default { :id="groupId(group)" :key="getGroupId(group)" data-testid="stage-column-group" - class="gl-relative gl-mb-3 gl-white-space-normal gl-pipeline-job-width" + class="gl-relative gl-white-space-normal gl-pipeline-job-width" + :class="{ + 'gl-mb-3': !isNewPipelineGraph, + 'gl-mb-2': isNewPipelineGraph, + }" @mouseenter="$emit('jobHover', group.name)" @mouseleave="$emit('jobHover', '')" > @@ -174,7 +218,7 @@ export default { :pipeline-expanded="pipelineExpanded" :pipeline-id="pipelineId" :stage-name="showStageName ? group.stageName : ''" - :css-class-job-name="$options.jobClasses" + :css-class-job-name="jobClasses" :class="[ { 'gl-opacity-3': isFadedOut(group.name) }, 'gl-transition-duration-slow gl-transition-timing-function-ease', @@ -188,7 +232,7 @@ export default { :group="group" :stage-name="showStageName ? group.stageName : ''" :pipeline-id="pipelineId" - :css-class-job-name="$options.jobClasses" + :css-class-job-name="jobClasses" /> </div> </div> diff --git a/app/assets/javascripts/diffs/components/app.vue b/app/assets/javascripts/diffs/components/app.vue index a389ab8e355..7c3d6dc8c42 100644 --- a/app/assets/javascripts/diffs/components/app.vue +++ b/app/assets/javascripts/diffs/components/app.vue @@ -1,6 +1,7 @@ <script> import { GlLoadingIcon, GlPagination, GlSprintf, GlAlert } from '@gitlab/ui'; import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils'; +import { debounce } from 'lodash'; // eslint-disable-next-line no-restricted-imports import { mapState, mapGetters, mapActions } from 'vuex'; import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; @@ -16,7 +17,8 @@ import { import { createAlert } from '~/alert'; import { isSingleViewStyle } from '~/helpers/diffs_helper'; import { helpPagePath } from '~/helpers/help_page_helper'; -import { parseBoolean } from '~/lib/utils/common_utils'; +import { parseBoolean, handleLocationHash } from '~/lib/utils/common_utils'; +import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants'; import { Mousetrap } from '~/lib/mousetrap'; import { updateHistory } from '~/lib/utils/url_utility'; import { __ } from '~/locale'; @@ -39,6 +41,7 @@ import { TRACKING_SINGLE_FILE_MODE, TRACKING_MULTIPLE_FILES_MODE, EVT_MR_PREPARED, + EVT_DISCUSSIONS_ASSIGNED, } from '../constants'; import { isCollapsed } from '../utils/diff_file'; @@ -136,6 +139,7 @@ export default { diffFilesLength: 0, virtualScrollCurrentIndex: -1, subscribedToVirtualScrollingEvents: false, + autoScrolled: false, }; }, apollo: { @@ -358,6 +362,10 @@ export default { this.adjustView(); this.subscribeToEvents(); + this.slowHashHandler = debounce(() => { + handleLocationHash(); + this.autoScrolled = true; + }, DEFAULT_DEBOUNCE_AND_THROTTLE_MS); this.unwatchDiscussions = this.$watch( () => `${this.flatBlobsList.length}:${this.$store.state.notes.discussions.length}`, () => { @@ -420,8 +428,10 @@ export default { diffsEventHub.$on('diffFilesModified', this.setDiscussions); diffsEventHub.$on('doneLoadingBatches', this.autoScroll); diffsEventHub.$on(EVT_MR_PREPARED, this.fetchData); + diffsEventHub.$on(EVT_DISCUSSIONS_ASSIGNED, this.handleHash); }, unsubscribeFromEvents() { + diffsEventHub.$off(EVT_DISCUSSIONS_ASSIGNED, this.handleHash); diffsEventHub.$off(EVT_MR_PREPARED, this.fetchData); diffsEventHub.$off('doneLoadingBatches', this.autoScroll); diffsEventHub.$off('diffFilesModified', this.setDiscussions); @@ -449,6 +459,15 @@ export default { .catch(() => {}); } }, + handleHash() { + if (this.viewDiffsFileByFile && !this.autoScrolled) { + const file = this.diffs[0]; + + if (file && !file.isLoadingFullFile) { + requestIdleCallback(() => this.slowHashHandler()); + } + } + }, navigateToDiffFileNumber(number) { this.navigateToDiffFileIndex(number - 1); }, diff --git a/app/assets/javascripts/diffs/constants.js b/app/assets/javascripts/diffs/constants.js index 575cd05ceb8..e48eb10753c 100644 --- a/app/assets/javascripts/diffs/constants.js +++ b/app/assets/javascripts/diffs/constants.js @@ -82,6 +82,7 @@ export const RENAMED_DIFF_TRANSITIONS = { // MR Diffs known events export const EVT_MR_PREPARED = 'mr:asyncPreparationFinished'; export const EVT_EXPAND_ALL_FILES = 'mr:diffs:expandAllFiles'; +export const EVT_DISCUSSIONS_ASSIGNED = 'mr:diffs:discussionsAssigned'; export const EVT_PERF_MARK_FILE_TREE_START = 'mr:diffs:perf:fileTreeStart'; export const EVT_PERF_MARK_FILE_TREE_END = 'mr:diffs:perf:fileTreeEnd'; export const EVT_PERF_MARK_DIFF_FILES_START = 'mr:diffs:perf:filesStart'; diff --git a/app/assets/javascripts/diffs/store/actions.js b/app/assets/javascripts/diffs/store/actions.js index ed8ae795bda..d86a88f97b8 100644 --- a/app/assets/javascripts/diffs/store/actions.js +++ b/app/assets/javascripts/diffs/store/actions.js @@ -49,6 +49,7 @@ import { TRACKING_MULTIPLE_FILES_MODE, EVT_MR_PREPARED, FILE_DIFF_POSITION_TYPE, + EVT_DISCUSSIONS_ASSIGNED, } from '../constants'; import { DISCUSSION_SINGLE_DIFF_FAILED, @@ -413,7 +414,7 @@ export const assignDiscussionsToDiff = ( } Vue.nextTick(() => { - notesEventHub.$emit('scrollToDiscussion'); + eventHub.$emit(EVT_DISCUSSIONS_ASSIGNED); }); }; diff --git a/app/assets/javascripts/search/sidebar/components/blobs_filters.vue b/app/assets/javascripts/search/sidebar/components/blobs_filters.vue index ac36ae6b366..0ed2c24efba 100644 --- a/app/assets/javascripts/search/sidebar/components/blobs_filters.vue +++ b/app/assets/javascripts/search/sidebar/components/blobs_filters.vue @@ -18,11 +18,8 @@ export default { computed: { ...mapGetters(['currentScope']), ...mapState(['useSidebarNavigation', 'searchType']), - showArchivedFilter() { - return this.glFeatures.searchBlobsHideArchivedProjects; - }, showDivider() { - return !this.useSidebarNavigation && this.showArchivedFilter; + return !this.useSidebarNavigation; }, hrClasses() { return [...HR_DEFAULT_CLASSES, 'gl-display-none', 'gl-md-display-block']; @@ -35,6 +32,6 @@ export default { <filters-template> <language-filter class="gl-mb-5" /> <hr v-if="showDivider" :class="hrClasses" /> - <archived-filter v-if="showArchivedFilter" class="gl-mb-5" /> + <archived-filter class="gl-mb-5" /> </filters-template> </template> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/checks/constants.js b/app/assets/javascripts/vue_merge_request_widget/components/checks/constants.js new file mode 100644 index 00000000000..e15987d7280 --- /dev/null +++ b/app/assets/javascripts/vue_merge_request_widget/components/checks/constants.js @@ -0,0 +1,6 @@ +export const COMPONENTS = { + conflicts: () => import('./conflicts.vue'), + unresolved_discussions: () => import('./unresolved_discussions.vue'), + rebase: () => import('./rebase.vue'), + default: () => import('./message.vue'), +}; diff --git a/app/assets/javascripts/vue_merge_request_widget/components/checks/unresolved_discussions.vue b/app/assets/javascripts/vue_merge_request_widget/components/checks/unresolved_discussions.vue new file mode 100644 index 00000000000..c8dba36700c --- /dev/null +++ b/app/assets/javascripts/vue_merge_request_widget/components/checks/unresolved_discussions.vue @@ -0,0 +1,37 @@ +<script> +import { s__ } from '~/locale'; +import notesEventHub from '~/notes/event_hub'; +import ActionButtons from '../action_buttons.vue'; +import MergeChecksMessage from './message.vue'; + +export default { + name: 'MergeChecksUnresolvedDiscussions', + components: { + MergeChecksMessage, + ActionButtons, + }, + props: { + check: { + type: Object, + required: true, + }, + }, + computed: { + tertiaryActionsButtons() { + return [ + { + text: s__('mrWidget|Go to first unresolved thread'), + category: 'default', + onClick: () => notesEventHub.$emit('jumpToFirstUnresolvedDiscussion'), + }, + ]; + }, + }, +}; +</script> + +<template> + <merge-checks-message :check="check"> + <action-buttons v-if="check.result === 'failed'" :tertiary-buttons="tertiaryActionsButtons" /> + </merge-checks-message> +</template> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/merge_checks.vue b/app/assets/javascripts/vue_merge_request_widget/components/merge_checks.vue index 5652b81386f..ecf2987307c 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/merge_checks.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/merge_checks.vue @@ -1,17 +1,12 @@ <script> import { GlSkeletonLoader } from '@gitlab/ui'; -import { n__, __, sprintf } from '~/locale'; +import { __, n__, sprintf } from '~/locale'; +import { COMPONENTS } from '~/vue_merge_request_widget/components/checks/constants'; import mergeRequestQueryVariablesMixin from '../mixins/merge_request_query_variables'; import mergeChecksQuery from '../queries/merge_checks.query.graphql'; import StateContainer from './state_container.vue'; import BoldText from './bold_text.vue'; -const COMPONENTS = { - conflicts: () => import('./checks/conflicts.vue'), - rebase: () => import('./checks/rebase.vue'), - default: () => import('./checks/message.vue'), -}; - export default { apollo: { state: { diff --git a/app/assets/javascripts/vue_merge_request_widget/components/widget/dynamic_content.vue b/app/assets/javascripts/vue_merge_request_widget/components/widget/dynamic_content.vue index 72c041759d9..d4375690ad1 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/widget/dynamic_content.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/widget/dynamic_content.vue @@ -1,5 +1,5 @@ <script> -import { GlBadge, GlLink } from '@gitlab/ui'; +import { GlBadge, GlLink, GlTooltipDirective } from '@gitlab/ui'; import SafeHtml from '~/vue_shared/directives/safe_html'; import { generateText } from '../extensions/utils'; import ContentRow from './widget_content_row.vue'; @@ -15,6 +15,7 @@ export default { }, directives: { SafeHtml, + GlTooltip: GlTooltipDirective, }, props: { data: { @@ -78,7 +79,11 @@ export default { <div class="gl-display-flex gl-flex-grow-1"> <div class="gl-display-flex gl-flex-grow-1 gl-align-items-baseline"> <div> - <p v-safe-html="generatedText" class="gl-mb-0 gl-mr-1"></p> + <p + v-gl-tooltip="{ title: data.tooltipText, boundary: 'viewport' }" + v-safe-html="generatedText" + class="gl-mb-0 gl-mr-1" + ></p> <gl-link v-if="data.link" :href="data.link.href">{{ data.link.text }}</gl-link> <p v-if="data.supportingText" diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index 31d45ad3a28..9a1faf27143 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -811,7 +811,7 @@ $ci-action-icon-size: 22px; $ci-action-icon-size-lg: 24px; $pipeline-dropdown-line-height: 20px; $ci-action-dropdown-button-size: 24px; -$ci-action-dropdown-svg-size: 12px; +$ci-action-dropdown-svg-size: 16px; /* CI variable lists diff --git a/app/assets/stylesheets/page_bundles/pipeline.scss b/app/assets/stylesheets/page_bundles/pipeline.scss index 98e9e2b3c27..dcd8f90ab1c 100644 --- a/app/assets/stylesheets/page_bundles/pipeline.scss +++ b/app/assets/stylesheets/page_bundles/pipeline.scss @@ -125,21 +125,27 @@ // They are here to still access a variable or because they use magic values. // scoped to the graph. Do not add other styles. .gl-pipeline-min-h { - min-height: $dropdown-max-height-lg; + min-height: calc(#{$dropdown-max-height-lg} + #{$gl-spacing-scale-6}); } .gl-pipeline-job-width { width: 100%; - max-width: 400px; } .gl-pipeline-job-width\! { width: 100% !important; - max-width: 400px !important; } .gl-downstream-pipeline-job-width { width: 8rem; + + .pipeline-graph-container & { + width: 100%; + + @media (min-width: $breakpoint-sm) { + width: 8rem; + } + } } .gl-linked-pipeline-padding { @@ -154,8 +160,8 @@ // Action Icons in big pipeline-graph nodes &.ci-action-icon-wrapper { - height: 30px; - width: 30px; + height: 24px; + width: 24px; border-radius: 100%; display: block; padding: 0; @@ -242,3 +248,64 @@ } } } + +.pipeline-graph-container { + .stage-column.is-stage-view:not(:last-of-type)::after { + content: ""; + position: absolute; + top: 100%; + left: $gl-spacing-scale-6; + width: 2px; + height: $gl-spacing-scale-5 * 2; + background-color: $gray-200; + + @media (min-width: $breakpoint-sm) { + top: 1.25rem; + left: 100%; + width: $gl-spacing-scale-5 * 2; + height: 2px; + } + } + + .stage-column, + .stage-column.is-stage-view { + @media (min-width: $breakpoint-sm) { + &:first-of-type { + margin-left: $gl-spacing-scale-6; + } + } + } + + .linked-pipeline-container[aria-expanded=true] { + @media (max-width: $breakpoint-sm) { + width: 100%; + + > div { + border-bottom-left-radius: 0; + } + + > div > button { + border-bottom-right-radius: 0 !important; + } + } + } + + .linked-pipelines-column, + .pipeline-show-container, + .pipeline-links-container { + @media (max-width: $breakpoint-sm) { + width: 100%; + } + } + + .pipeline-graph { + @media (max-width: $breakpoint-sm) { + border-top-left-radius: 0; + border-top-right-radius: 0; + } + } + + .pipeline-graph .pipeline-graph { + background-color: $gray-100; + } +} diff --git a/app/controllers/projects/group_links_controller.rb b/app/controllers/projects/group_links_controller.rb index 60300f78bbb..5f8bf423219 100644 --- a/app/controllers/projects/group_links_controller.rb +++ b/app/controllers/projects/group_links_controller.rb @@ -9,30 +9,47 @@ class Projects::GroupLinksController < Projects::ApplicationController feature_category :groups_and_projects def update - Projects::GroupLinks::UpdateService.new(group_link, current_user).execute(group_link_params) + result = Projects::GroupLinks::UpdateService.new(group_link, current_user).execute(group_link_params) - if group_link.expires? - render json: { - expires_in: helpers.time_ago_with_tooltip(group_link.expires_at), - expires_soon: group_link.expires_soon? - } - else - render json: {} + if result.success? + if group_link.expires? + render json: { + expires_in: helpers.time_ago_with_tooltip(group_link.expires_at), + expires_soon: group_link.expires_soon? + } + else + render json: {} + end + elsif result.reason == :not_found + render json: { message: result.message }, status: :not_found end end def destroy - ::Projects::GroupLinks::DestroyService.new(project, current_user).execute(group_link) - - respond_to do |format| - format.html do - if can?(current_user, :admin_group, group_link.group) - redirect_to group_path(group_link.group), status: :found - elsif can?(current_user, :admin_project, group_link.project) - redirect_to project_project_members_path(project), status: :found + result = ::Projects::GroupLinks::DestroyService.new(project, current_user).execute(group_link) + + if result.success? + respond_to do |format| + format.html do + if can?(current_user, :admin_group, group_link.group) + redirect_to group_path(group_link.group), status: :found + elsif can?(current_user, :admin_project, group_link.project) + redirect_to project_project_members_path(project), status: :found + end + end + format.js { head :ok } + end + else + respond_to do |format| + format.html do + redirect_to project_project_members_path(project, tab: :groups), status: :found, + alert: _('The project-group link could not be removed.') + end + + format.js do + render json: { message: result.message }, status: :not_found if result.reason == :not_found end end - format.js { head :ok } end end diff --git a/app/helpers/visibility_level_helper.rb b/app/helpers/visibility_level_helper.rb index 68b15f7e042..cddfc48c649 100644 --- a/app/helpers/visibility_level_helper.rb +++ b/app/helpers/visibility_level_helper.rb @@ -76,16 +76,6 @@ module VisibilityLevelHelper end end - def visibility_level_options(form_model) - available_visibility_levels(form_model).map do |level| - { - level: level, - label: visibility_level_label(level), - description: visibility_level_description(level, form_model) - } - end - end - def snippets_selected_visibility_level(visibility_levels, selected) visibility_levels.find { |level| level == selected } || visibility_levels.min end diff --git a/app/models/ci/sources/pipeline.rb b/app/models/ci/sources/pipeline.rb index 5b6946b04fd..475d57ee4c8 100644 --- a/app/models/ci/sources/pipeline.rb +++ b/app/models/ci/sources/pipeline.rb @@ -12,7 +12,7 @@ module Ci :pipeline_id_convert_to_bigint, :source_pipeline_id_convert_to_bigint ], remove_with: '16.6', remove_after: '2023-10-22' - columns_changing_default :partition_id + columns_changing_default :partition_id, :source_partition_id self.table_name = "ci_sources_pipelines" diff --git a/app/models/integration.rb b/app/models/integration.rb index b4408301c6d..9b268e10cc6 100644 --- a/app/models/integration.rb +++ b/app/models/integration.rb @@ -237,6 +237,18 @@ class Integration < ApplicationRecord end private_class_method :boolean_accessor + def self.title + raise NotImplementedError + end + + def self.description + raise NotImplementedError + end + + def self.help + # no-op + end + def self.to_param raise NotImplementedError end @@ -447,19 +459,18 @@ class Integration < ApplicationRecord end def title - # implement inside child + self.class.title end def description - # implement inside child + self.class.description end def help - # implement inside child + self.class.help end def to_param - # implement inside child self.class.to_param end diff --git a/app/models/integrations/apple_app_store.rb b/app/models/integrations/apple_app_store.rb index ef12fc6bf6f..f8fddf8a457 100644 --- a/app/models/integrations/apple_app_store.rb +++ b/app/models/integrations/apple_app_store.rb @@ -37,15 +37,15 @@ module Integrations title: -> { s_('AppleAppStore|Protected branches and tags only') }, checkbox_label: -> { s_('AppleAppStore|Only set variables on protected branches and tags') } - def title + def self.title 'Apple App Store Connect' end - def description + def self.description s_('AppleAppStore|Use GitLab to build and release an app in the Apple App Store.') end - def help + def self.help variable_list = [ '<code>APP_STORE_CONNECT_API_KEY_ISSUER_ID</code>', '<code>APP_STORE_CONNECT_API_KEY_KEY_ID</code>', diff --git a/app/models/integrations/asana.rb b/app/models/integrations/asana.rb index 77555996cd9..39407acd6c9 100644 --- a/app/models/integrations/asana.rb +++ b/app/models/integrations/asana.rb @@ -20,15 +20,15 @@ module Integrations title: -> { s_('Integrations|Restrict to branch (optional)') }, help: -> { s_('AsanaService|Comma-separated list of branches to be automatically inspected. Leave blank to include all branches.') } - def title + def self.title 'Asana' end - def description + def self.description s_('AsanaService|Add commit messages as comments to Asana tasks.') end - def help + def self.help docs_link = ActionController::Base.helpers.link_to _('Learn more.'), Rails.application.routes.url_helpers.help_page_url('user/project/integrations/asana'), target: '_blank', rel: 'noopener noreferrer' s_('Add commit messages as comments to Asana tasks. %{docs_link}').html_safe % { docs_link: docs_link.html_safe } end diff --git a/app/models/integrations/assembla.rb b/app/models/integrations/assembla.rb index 1d3616b4c3b..bbdd0e183f2 100644 --- a/app/models/integrations/assembla.rb +++ b/app/models/integrations/assembla.rb @@ -15,11 +15,11 @@ module Integrations exposes_secrets: true, placeholder: '' - def title + def self.title 'Assembla' end - def description + def self.description _('Manage projects.') end diff --git a/app/models/integrations/bamboo.rb b/app/models/integrations/bamboo.rb index 9f15532a0b0..9fe73f86be3 100644 --- a/app/models/integrations/bamboo.rb +++ b/app/models/integrations/bamboo.rb @@ -38,15 +38,15 @@ module Integrations attr_accessor :response - def title + def self.title s_('BambooService|Atlassian Bamboo') end - def description + def self.description s_('BambooService|Run CI/CD pipelines with Atlassian Bamboo.') end - def help + def self.help docs_link = ActionController::Base.helpers.link_to( _('Learn more.'), Rails.application.routes.url_helpers.help_page_url('user/project/integrations/bamboo'), diff --git a/app/models/integrations/base_chat_notification.rb b/app/models/integrations/base_chat_notification.rb index b75801335bd..167bc210349 100644 --- a/app/models/integrations/base_chat_notification.rb +++ b/app/models/integrations/base_chat_notification.rb @@ -136,10 +136,6 @@ module Integrations raise NotImplementedError end - def help - raise NotImplementedError - end - # With some integrations the webhook is already tied to a specific channel, # for others the channels are configurable for each event. def configurable_channels? diff --git a/app/models/integrations/base_slack_notification.rb b/app/models/integrations/base_slack_notification.rb index 09a0c9ba361..33dd9d9d387 100644 --- a/app/models/integrations/base_slack_notification.rb +++ b/app/models/integrations/base_slack_notification.rb @@ -36,7 +36,7 @@ module Integrations true end - def help + def self.help # noop end diff --git a/app/models/integrations/bugzilla.rb b/app/models/integrations/bugzilla.rb index 74e282f6848..3ca348e42a1 100644 --- a/app/models/integrations/bugzilla.rb +++ b/app/models/integrations/bugzilla.rb @@ -6,15 +6,15 @@ module Integrations validates :project_url, :issues_url, :new_issue_url, presence: true, public_url: true, if: :activated? - def title + def self.title 'Bugzilla' end - def description + def self.description s_("IssueTracker|Use Bugzilla as this project's issue tracker.") end - def help + def self.help docs_link = ActionController::Base.helpers.link_to _('Learn more.'), Rails.application.routes.url_helpers.help_page_url('user/project/integrations/bugzilla'), target: '_blank', rel: 'noopener noreferrer' s_("IssueTracker|Use Bugzilla as this project's issue tracker. %{docs_link}").html_safe % { docs_link: docs_link.html_safe } end diff --git a/app/models/integrations/buildkite.rb b/app/models/integrations/buildkite.rb index 82a5142e8c2..aab0cdf2134 100644 --- a/app/models/integrations/buildkite.rb +++ b/app/models/integrations/buildkite.rb @@ -75,20 +75,20 @@ module Integrations "#{project_url}/builds?commit=#{sha}" end - def title + def self.title 'Buildkite' end - def description + def self.description 'Run CI/CD pipelines with Buildkite.' end - def self.to_param - 'buildkite' + def self.help + s_('ProjectService|Run CI/CD pipelines with Buildkite.') end - def help - s_('ProjectService|Run CI/CD pipelines with Buildkite.') + def self.to_param + 'buildkite' end def calculate_reactive_cache(sha, ref) diff --git a/app/models/integrations/campfire.rb b/app/models/integrations/campfire.rb index 8b5797a9d24..18268ed18f4 100644 --- a/app/models/integrations/campfire.rb +++ b/app/models/integrations/campfire.rb @@ -36,15 +36,15 @@ module Integrations placeholder: '123456', help: -> { s_('CampfireService|From the end of the room URL.') } - def title + def self.title 'Campfire' end - def description + def self.description 'Send notifications about push events to Campfire chat rooms.' end - def help + def self.help docs_link = ActionController::Base.helpers.link_to( _('Learn more.'), Rails.application.routes.url_helpers.help_page_url('api/integrations', anchor: 'campfire'), diff --git a/app/models/integrations/clickup.rb b/app/models/integrations/clickup.rb index 7cc05d41e14..25287b53300 100644 --- a/app/models/integrations/clickup.rb +++ b/app/models/integrations/clickup.rb @@ -10,15 +10,15 @@ module Integrations @reference_pattern ||= /((#|CU-)(?<issue>[a-z0-9]+)|(?<issue>[A-Z0-9_]{2,10}-\d+))\b/ end - def title + def self.title 'ClickUp' end - def description + def self.description s_("IssueTracker|Use Clickup as this project's issue tracker.") end - def help + def self.help docs_link = ActionController::Base.helpers.link_to _('Learn more.'), Rails.application.routes.url_helpers.help_page_url('user/project/integrations/clickup'), target: '_blank', diff --git a/app/models/integrations/confluence.rb b/app/models/integrations/confluence.rb index eda8c37fc72..f97f1fd25c9 100644 --- a/app/models/integrations/confluence.rb +++ b/app/models/integrations/confluence.rb @@ -22,11 +22,11 @@ module Integrations 'confluence' end - def title + def self.title s_('ConfluenceService|Confluence Workspace') end - def description + def self.description s_('ConfluenceService|Link to a Confluence Workspace from the sidebar.') end diff --git a/app/models/integrations/custom_issue_tracker.rb b/app/models/integrations/custom_issue_tracker.rb index 3770e813eaa..fe0d01d60bd 100644 --- a/app/models/integrations/custom_issue_tracker.rb +++ b/app/models/integrations/custom_issue_tracker.rb @@ -6,15 +6,15 @@ module Integrations validates :project_url, :issues_url, :new_issue_url, presence: true, public_url: true, if: :activated? - def title + def self.title s_('IssueTracker|Custom issue tracker') end - def description + def self.description s_("IssueTracker|Use a custom issue tracker as this project's issue tracker.") end - def help + def self.help docs_link = ActionController::Base.helpers.link_to _('Learn more.'), Rails.application.routes.url_helpers.help_page_url('user/project/integrations/custom_issue_tracker'), target: '_blank', rel: 'noopener noreferrer' s_('IssueTracker|Use a custom issue tracker that is not in the integration list. %{docs_link}').html_safe % { docs_link: docs_link.html_safe } end diff --git a/app/models/integrations/datadog.rb b/app/models/integrations/datadog.rb index b1f1361afcd..5682fc2b139 100644 --- a/app/models/integrations/datadog.rb +++ b/app/models/integrations/datadog.rb @@ -117,15 +117,15 @@ module Integrations # archive_trace is opt-in but we handle it with a more detailed field below end - def title + def self.title 'Datadog' end - def description + def self.description s_('DatadogIntegration|Trace your GitLab pipelines with Datadog.') end - def help + def self.help docs_link = ActionController::Base.helpers.link_to( s_('DatadogIntegration|How do I set up this integration?'), Rails.application.routes.url_helpers.help_page_url('integration/datadog'), diff --git a/app/models/integrations/discord.rb b/app/models/integrations/discord.rb index 33b2b52fa62..7ce597389f0 100644 --- a/app/models/integrations/discord.rb +++ b/app/models/integrations/discord.rb @@ -21,23 +21,23 @@ module Integrations title: -> { s_('Integrations|Branches for which notifications are to be sent') }, choices: -> { branch_choices } - def title + def self.title s_("DiscordService|Discord Notifications") end - def description + def self.description s_("DiscordService|Send notifications about project events to a Discord channel.") end - def self.to_param - "discord" - end - - def help + def self.help docs_link = ActionController::Base.helpers.link_to _('How do I set up this service?'), Rails.application.routes.url_helpers.help_page_url('user/project/integrations/discord_notifications'), target: '_blank', rel: 'noopener noreferrer' s_('Send notifications about project events to a Discord channel. %{docs_link}').html_safe % { docs_link: docs_link.html_safe } end + def self.to_param + "discord" + end + def default_channel_placeholder s_('DiscordService|Override the default webhook (e.g. https://discord.com/api/webhooks/…)') end diff --git a/app/models/integrations/drone_ci.rb b/app/models/integrations/drone_ci.rb index f6a12c4bb1a..b59e504c98f 100644 --- a/app/models/integrations/drone_ci.rb +++ b/app/models/integrations/drone_ci.rb @@ -87,20 +87,20 @@ module Integrations "gitlab/#{project.full_path}/redirect/commits/#{sha}?branch=#{Addressable::URI.encode_component(ref.to_s)}") end - def title + def self.title 'Drone' end - def description + def self.description s_('ProjectService|Run CI/CD pipelines with Drone.') end - def self.to_param - 'drone_ci' + def self.help + s_('ProjectService|Run CI/CD pipelines with Drone.') end - def help - s_('ProjectService|Run CI/CD pipelines with Drone.') + def self.to_param + 'drone_ci' end override :hook_url diff --git a/app/models/integrations/emails_on_push.rb b/app/models/integrations/emails_on_push.rb index 144d1a07b04..77be8f5db45 100644 --- a/app/models/integrations/emails_on_push.rb +++ b/app/models/integrations/emails_on_push.rb @@ -39,11 +39,11 @@ module Integrations recipients.split.grep(Devise.email_regexp).uniq(&:downcase) end - def title + def self.title s_('EmailsOnPushService|Emails on push') end - def description + def self.description s_('EmailsOnPushService|Email the commits and diff of each push to a list of recipients.') end diff --git a/app/models/integrations/ewm.rb b/app/models/integrations/ewm.rb index 003c896704a..9d6f4c2a56c 100644 --- a/app/models/integrations/ewm.rb +++ b/app/models/integrations/ewm.rb @@ -10,15 +10,15 @@ module Integrations @reference_pattern ||= %r{(?<issue>\b(bug|task|work item|workitem|rtcwi|defect)\b\s+\d+)}i end - def title + def self.title 'EWM' end - def description + def self.description s_("IssueTracker|Use IBM Engineering Workflow Management as this project's issue tracker.") end - def help + def self.help docs_link = ActionController::Base.helpers.link_to _('Learn more.'), Rails.application.routes.url_helpers.help_page_url('user/project/integrations/ewm'), target: '_blank', rel: 'noopener noreferrer' s_("IssueTracker|Use IBM Engineering Workflow Management as this project's issue tracker. %{docs_link}").html_safe % { docs_link: docs_link.html_safe } end diff --git a/app/models/integrations/external_wiki.rb b/app/models/integrations/external_wiki.rb index acacab2528e..7408f86d231 100644 --- a/app/models/integrations/external_wiki.rb +++ b/app/models/integrations/external_wiki.rb @@ -11,24 +11,24 @@ module Integrations help: -> { s_('ExternalWikiService|Enter the URL to the external wiki.') }, required: true - def title + def self.title s_('ExternalWikiService|External wiki') end - def description + def self.description s_('ExternalWikiService|Link to an external wiki from the sidebar.') end - def self.to_param - 'external_wiki' - end - - def help + def self.help docs_link = ActionController::Base.helpers.link_to _('Learn more.'), Rails.application.routes.url_helpers.help_page_url('user/project/wiki/index', anchor: 'link-an-external-wiki'), target: '_blank', rel: 'noopener noreferrer' s_('Link an external wiki from the project\'s sidebar. %{docs_link}').html_safe % { docs_link: docs_link.html_safe } end + def self.to_param + 'external_wiki' + end + def sections [ { diff --git a/app/models/integrations/gitlab_slack_application.rb b/app/models/integrations/gitlab_slack_application.rb index 2d520eaf7e7..d008a28a226 100644 --- a/app/models/integrations/gitlab_slack_application.rb +++ b/app/models/integrations/gitlab_slack_application.rb @@ -26,11 +26,11 @@ module Integrations update(active: !!slack_integration) end - def title + def self.title s_('Integrations|GitLab for Slack app') end - def description + def self.description s_('Integrations|Enable slash commands and notifications for a Slack workspace.') end diff --git a/app/models/integrations/google_play.rb b/app/models/integrations/google_play.rb index 5389e8dfa81..746f68fdc4c 100644 --- a/app/models/integrations/google_play.rb +++ b/app/models/integrations/google_play.rb @@ -32,15 +32,15 @@ module Integrations title: -> { s_('GooglePlayStore|Protected branches and tags only') }, checkbox_label: -> { s_('GooglePlayStore|Only set variables on protected branches and tags') } - def title + def self.title s_('GooglePlay|Google Play') end - def description + def self.description s_('GooglePlay|Use GitLab to build and release an app in Google Play.') end - def help + def self.help variable_list = [ '<code>SUPPLY_PACKAGE_NAME</code>', '<code>SUPPLY_JSON_KEY_DATA</code>' diff --git a/app/models/integrations/hangouts_chat.rb b/app/models/integrations/hangouts_chat.rb index 6e4753470a3..6a9d603e6e5 100644 --- a/app/models/integrations/hangouts_chat.rb +++ b/app/models/integrations/hangouts_chat.rb @@ -17,11 +17,11 @@ module Integrations title: -> { s_('Integrations|Branches for which notifications are to be sent') }, choices: -> { branch_choices } - def title + def self.title 'Google Chat' end - def description + def self.description 'Send notifications from GitLab to a room in Google Chat.' end @@ -29,7 +29,7 @@ module Integrations 'hangouts_chat' end - def help + def self.help docs_link = ActionController::Base.helpers.link_to(_('How do I set up a Google Chat webhook?'), Rails.application.routes.url_helpers.help_page_url('user/project/integrations/hangouts_chat'), target: '_blank', rel: 'noopener noreferrer') diff --git a/app/models/integrations/harbor.rb b/app/models/integrations/harbor.rb index 559e48afd10..cc570e49e36 100644 --- a/app/models/integrations/harbor.rb +++ b/app/models/integrations/harbor.rb @@ -32,34 +32,32 @@ module Integrations non_empty_password_help: -> { s_('HarborIntegration|Leave blank to use your current password.') }, required: true - def title + def self.title 'Harbor' end - def description + def self.description s_("HarborIntegration|Use Harbor as this project's container registry.") end - def help + def self.help s_("HarborIntegration|After the Harbor integration is activated, global variables `$HARBOR_USERNAME`, `$HARBOR_HOST`, `$HARBOR_OCI`, `$HARBOR_PASSWORD`, `$HARBOR_URL` and `$HARBOR_PROJECT` will be created for CI/CD use.") end + def self.to_param + name.demodulize.downcase + end + def hostname Gitlab::Utils.parse_url(url).hostname end - class << self - def to_param - name.demodulize.downcase - end - - def supported_events - [] - end + def self.supported_events + [] + end - def supported_event_actions - [] - end + def self.supported_event_actions + [] end def test(*_args) diff --git a/app/models/integrations/irker.rb b/app/models/integrations/irker.rb index a54946f074a..a1ce0877957 100644 --- a/app/models/integrations/irker.rb +++ b/app/models/integrations/irker.rb @@ -53,14 +53,31 @@ module Integrations # in the UI or API. prop_accessor :channels - def title + def self.title s_('IrkerService|irker (IRC gateway)') end - def description + def self.description s_('IrkerService|Send update messages to an irker server.') end + def self.help + docs_link = ActionController::Base.helpers.link_to( + _('Learn more.'), + Rails.application.routes.url_helpers.help_page_url( + 'user/project/integrations/irker', + anchor: 'set-up-an-irker-daemon' + ), + target: '_blank', + rel: 'noopener noreferrer' + ) + + format(s_( + 'IrkerService|Send update messages to an irker server. ' \ + 'Before you can use this, you need to set up the irker daemon. %{docs_link}' + ).html_safe, docs_link: docs_link.html_safe) + end + def self.to_param 'irker' end @@ -85,23 +102,6 @@ module Integrations } end - def help - docs_link = ActionController::Base.helpers.link_to( - _('Learn more.'), - Rails.application.routes.url_helpers.help_page_url( - 'user/project/integrations/irker', - anchor: 'set-up-an-irker-daemon' - ), - target: '_blank', - rel: 'noopener noreferrer' - ) - - format(s_( - 'IrkerService|Send update messages to an irker server. ' \ - 'Before you can use this, you need to set up the irker daemon. %{docs_link}' - ).html_safe, docs_link: docs_link.html_safe) - end - private def get_channels diff --git a/app/models/integrations/jenkins.rb b/app/models/integrations/jenkins.rb index 0683c8408bc..a2f5667eaee 100644 --- a/app/models/integrations/jenkins.rb +++ b/app/models/integrations/jenkins.rb @@ -69,15 +69,15 @@ module Integrations %w[push merge_request tag_push] end - def title + def self.title 'Jenkins' end - def description + def self.description s_('Run CI/CD pipelines with Jenkins.') end - def help + def self.help docs_link = ActionController::Base.helpers.link_to _('Learn more.'), Rails.application.routes.url_helpers.help_page_url('integration/jenkins'), target: '_blank', rel: 'noopener noreferrer' s_('Run CI/CD pipelines with Jenkins when you push to a repository, or when a merge request is created, updated, or merged. %{docs_link}').html_safe % { docs_link: docs_link.html_safe } end diff --git a/app/models/integrations/jira.rb b/app/models/integrations/jira.rb index f6e99454cb1..37735b956fc 100644 --- a/app/models/integrations/jira.rb +++ b/app/models/integrations/jira.rb @@ -191,9 +191,17 @@ module Integrations end end - def help + def self.title + 'Jira' + end + + def self.description + s_("JiraService|Use Jira as this project's issue tracker.") + end + + def self.help jira_doc_link_start = format('<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe, - url: help_page_path('integration/jira/index')) + url: Gitlab::Routing.url_helpers.help_page_path('integration/jira/index')) format( s_("JiraService|You must configure Jira before enabling this integration. " \ "%{jira_doc_link_start}Learn more.%{link_end}"), @@ -201,14 +209,6 @@ module Integrations link_end: '</a>'.html_safe) end - def title - 'Jira' - end - - def description - s_("JiraService|Use Jira as this project's issue tracker.") - end - def self.to_param 'jira' end diff --git a/app/models/integrations/mattermost.rb b/app/models/integrations/mattermost.rb index 7e391b11d82..361ff4afce8 100644 --- a/app/models/integrations/mattermost.rb +++ b/app/models/integrations/mattermost.rb @@ -5,11 +5,11 @@ module Integrations include SlackMattermostNotifier include SlackMattermostFields - def title + def self.title _('Mattermost notifications') end - def description + def self.description s_('Send notifications about project events to Mattermost channels.') end @@ -17,7 +17,7 @@ module Integrations 'mattermost' end - def help + def self.help docs_link = ActionController::Base.helpers.link_to _('Learn more.'), Rails.application.routes.url_helpers.help_page_url('user/project/integrations/mattermost'), target: '_blank', rel: 'noopener noreferrer' s_('Send notifications about project events to Mattermost channels. %{docs_link}').html_safe % { docs_link: docs_link.html_safe } end diff --git a/app/models/integrations/mattermost_slash_commands.rb b/app/models/integrations/mattermost_slash_commands.rb index 73cddd163e0..9554dec4168 100644 --- a/app/models/integrations/mattermost_slash_commands.rb +++ b/app/models/integrations/mattermost_slash_commands.rb @@ -14,11 +14,11 @@ module Integrations false end - def title + def self.title s_('Integrations|Mattermost slash commands') end - def description + def self.description s_('Integrations|Perform common tasks with slash commands.') end diff --git a/app/models/integrations/microsoft_teams.rb b/app/models/integrations/microsoft_teams.rb index 208172d6303..3a7c848d411 100644 --- a/app/models/integrations/microsoft_teams.rb +++ b/app/models/integrations/microsoft_teams.rb @@ -18,11 +18,11 @@ module Integrations title: -> { s_('Integrations|Branches for which notifications are to be sent') }, choices: -> { branch_choices } - def title + def self.title 'Microsoft Teams notifications' end - def description + def self.description 'Send notifications about project events to Microsoft Teams.' end @@ -30,7 +30,7 @@ module Integrations 'microsoft_teams' end - def help + def self.help '<p>Use this service to send notifications about events in GitLab projects to your Microsoft Teams channels. <a href="https://docs.gitlab.com/ee/user/project/integrations/microsoft_teams.html" target="_blank" rel="noopener noreferrer">How do I configure this integration?</a></p>' end diff --git a/app/models/integrations/mock_ci.rb b/app/models/integrations/mock_ci.rb index 2d8e26d409f..9c129ca727c 100644 --- a/app/models/integrations/mock_ci.rb +++ b/app/models/integrations/mock_ci.rb @@ -14,11 +14,11 @@ module Integrations validates :mock_service_url, presence: true, public_url: true, if: :activated? - def title + def self.title 'MockCI' end - def description + def self.description 'Mock an external CI' end diff --git a/app/models/integrations/mock_monitoring.rb b/app/models/integrations/mock_monitoring.rb index 72bb292edaa..9e474078b28 100644 --- a/app/models/integrations/mock_monitoring.rb +++ b/app/models/integrations/mock_monitoring.rb @@ -2,11 +2,11 @@ module Integrations class MockMonitoring < BaseMonitoring - def title + def self.title 'Mock monitoring' end - def description + def self.description 'Mock monitoring service' end diff --git a/app/models/integrations/packagist.rb b/app/models/integrations/packagist.rb index c0acb6c87b4..f027afe0381 100644 --- a/app/models/integrations/packagist.rb +++ b/app/models/integrations/packagist.rb @@ -29,11 +29,11 @@ module Integrations validates :username, presence: true, if: :activated? validates :token, presence: true, if: :activated? - def title + def self.title 'Packagist' end - def description + def self.description s_('Integrations|Keep your PHP dependencies updated on Packagist.') end diff --git a/app/models/integrations/pipelines_email.rb b/app/models/integrations/pipelines_email.rb index 01efbc3e4a4..c7a93d48825 100644 --- a/app/models/integrations/pipelines_email.rb +++ b/app/models/integrations/pipelines_email.rb @@ -44,11 +44,11 @@ module Integrations end end - def title + def self.title _('Pipeline status emails') end - def description + def self.description _('Email the pipeline status to a list of recipients.') end diff --git a/app/models/integrations/pivotaltracker.rb b/app/models/integrations/pivotaltracker.rb index b3cbc988dd6..97e6e3e09d1 100644 --- a/app/models/integrations/pivotaltracker.rb +++ b/app/models/integrations/pivotaltracker.rb @@ -20,15 +20,15 @@ module Integrations 'automatically inspect. Leave blank to include all branches.') end - def title + def self.title 'Pivotal Tracker' end - def description + def self.description s_('PivotalTrackerService|Add commit messages as comments to Pivotal Tracker stories.') end - def help + def self.help docs_link = ActionController::Base.helpers.link_to _('Learn more.'), Rails.application.routes.url_helpers.help_page_url('user/project/integrations/pivotal_tracker'), target: '_blank', rel: 'noopener noreferrer' s_('Add commit messages as comments to Pivotal Tracker stories. %{docs_link}').html_safe % { docs_link: docs_link.html_safe } end diff --git a/app/models/integrations/prometheus.rb b/app/models/integrations/prometheus.rb index ff8d07a1b4c..de923bbbdd5 100644 --- a/app/models/integrations/prometheus.rb +++ b/app/models/integrations/prometheus.rb @@ -51,11 +51,11 @@ module Integrations false end - def title + def self.title 'Prometheus' end - def description + def self.description s_('PrometheusService|Monitor application health with Prometheus metrics and dashboards') end diff --git a/app/models/integrations/pumble.rb b/app/models/integrations/pumble.rb index 09e011023ed..36ff5189b0f 100644 --- a/app/models/integrations/pumble.rb +++ b/app/models/integrations/pumble.rb @@ -18,11 +18,11 @@ module Integrations title: -> { s_('Integrations|Branches for which notifications are to be sent') }, choices: -> { branch_choices } - def title + def self.title 'Pumble' end - def description + def self.description s_("PumbleIntegration|Send notifications about project events to Pumble.") end @@ -30,7 +30,7 @@ module Integrations 'pumble' end - def help + def self.help docs_link = ActionController::Base.helpers.link_to( _('Learn more.'), Rails.application.routes.url_helpers.help_page_url('user/project/integrations/pumble'), diff --git a/app/models/integrations/pushover.rb b/app/models/integrations/pushover.rb index 2feae29f627..b2c4e06e71f 100644 --- a/app/models/integrations/pushover.rb +++ b/app/models/integrations/pushover.rb @@ -71,11 +71,11 @@ module Integrations ] end - def title + def self.title 'Pushover' end - def description + def self.description s_('PushoverService|Get real-time notifications on your device.') end diff --git a/app/models/integrations/redmine.rb b/app/models/integrations/redmine.rb index bc2a64b0848..11eda7c69f7 100644 --- a/app/models/integrations/redmine.rb +++ b/app/models/integrations/redmine.rb @@ -6,15 +6,15 @@ module Integrations validates :project_url, :issues_url, :new_issue_url, presence: true, public_url: true, if: :activated? - def title + def self.title 'Redmine' end - def description + def self.description s_("IssueTracker|Use Redmine as this project's issue tracker.") end - def help + def self.help docs_link = ActionController::Base.helpers.link_to _('Learn more.'), Rails.application.routes.url_helpers.help_page_url('user/project/integrations/redmine'), target: '_blank', rel: 'noopener noreferrer' s_('IssueTracker|Use Redmine as the issue tracker. %{docs_link}').html_safe % { docs_link: docs_link.html_safe } end diff --git a/app/models/integrations/shimo.rb b/app/models/integrations/shimo.rb index 227fdca5c91..1d004356469 100644 --- a/app/models/integrations/shimo.rb +++ b/app/models/integrations/shimo.rb @@ -16,11 +16,11 @@ module Integrations valid? && activated? end - def title + def self.title s_('Shimo|Shimo') end - def description + def self.description s_('Shimo|Link to a Shimo Workspace from the sidebar.') end diff --git a/app/models/integrations/slack.rb b/app/models/integrations/slack.rb index f70376e2f0d..9f9614a84fd 100644 --- a/app/models/integrations/slack.rb +++ b/app/models/integrations/slack.rb @@ -5,11 +5,11 @@ module Integrations include SlackMattermostNotifier include SlackMattermostFields - def title + def self.title 'Slack notifications' end - def description + def self.description 'Send notifications about project events to Slack.' end diff --git a/app/models/integrations/slack_slash_commands.rb b/app/models/integrations/slack_slash_commands.rb index b209f37ee7c..c5ea6f22951 100644 --- a/app/models/integrations/slack_slash_commands.rb +++ b/app/models/integrations/slack_slash_commands.rb @@ -10,11 +10,11 @@ module Integrations non_empty_password_help: -> { s_('ProjectService|Leave blank to use your current token.') }, placeholder: '' - def title + def self.title 'Slack slash commands' end - def description + def self.description "Perform common operations in Slack." end diff --git a/app/models/integrations/squash_tm.rb b/app/models/integrations/squash_tm.rb index bf3f391564f..1b4ab152b1d 100644 --- a/app/models/integrations/squash_tm.rb +++ b/app/models/integrations/squash_tm.rb @@ -22,15 +22,15 @@ module Integrations validates :token, length: { maximum: 255 }, allow_blank: true end - def title + def self.title 'Squash TM' end - def description + def self.description s_("SquashTmIntegration|Update Squash TM requirements when GitLab issues are modified.") end - def help + def self.help docs_link = ActionController::Base.helpers.link_to( _('Learn more.'), Rails.application.routes.url_helpers.help_page_url('user/project/integrations/squash_tm'), diff --git a/app/models/integrations/teamcity.rb b/app/models/integrations/teamcity.rb index 575c3b8a334..913242ef9ac 100644 --- a/app/models/integrations/teamcity.rb +++ b/app/models/integrations/teamcity.rb @@ -47,15 +47,15 @@ module Integrations end end - def title + def self.title 'JetBrains TeamCity' end - def description + def self.description s_('ProjectService|Run CI/CD pipelines with JetBrains TeamCity.') end - def help + def self.help s_('To run CI/CD pipelines with JetBrains TeamCity, input the GitLab project details in the TeamCity project Version Control Settings.') end diff --git a/app/models/integrations/telegram.rb b/app/models/integrations/telegram.rb index 71fe6f8d6ef..8eb1a7ad0ea 100644 --- a/app/models/integrations/telegram.rb +++ b/app/models/integrations/telegram.rb @@ -38,11 +38,11 @@ module Integrations before_validation :set_webhook - def title + def self.title 'Telegram' end - def description + def self.description s_("TelegramIntegration|Send notifications about project events to Telegram.") end @@ -50,7 +50,7 @@ module Integrations 'telegram' end - def help + def self.help docs_link = ActionController::Base.helpers.link_to( _('Learn more.'), Rails.application.routes.url_helpers.help_page_url('user/project/integrations/telegram'), diff --git a/app/models/integrations/unify_circuit.rb b/app/models/integrations/unify_circuit.rb index 3b4bcfa28d3..6ee95c1173b 100644 --- a/app/models/integrations/unify_circuit.rb +++ b/app/models/integrations/unify_circuit.rb @@ -17,11 +17,11 @@ module Integrations title: -> { s_('Integrations|Branches for which notifications are to be sent') }, choices: -> { branch_choices } - def title + def self.title 'Unify Circuit' end - def description + def self.description s_('Integrations|Send notifications about project events to Unify Circuit.') end @@ -29,7 +29,7 @@ module Integrations 'unify_circuit' end - def help + def self.help docs_link = ActionController::Base.helpers.link_to _('How do I set up this service?'), Rails.application.routes.url_helpers.help_page_url('user/project/integrations/unify_circuit'), target: '_blank', rel: 'noopener noreferrer' s_('Integrations|Send notifications about project events to a Unify Circuit conversation. %{docs_link}').html_safe % { docs_link: docs_link.html_safe } end diff --git a/app/models/integrations/webex_teams.rb b/app/models/integrations/webex_teams.rb index 3ef8ab39352..5f8cc195544 100644 --- a/app/models/integrations/webex_teams.rb +++ b/app/models/integrations/webex_teams.rb @@ -17,11 +17,11 @@ module Integrations title: -> { s_('Integrations|Branches for which notifications are to be sent') }, choices: -> { branch_choices } - def title + def self.title s_("WebexTeamsService|Webex Teams") end - def description + def self.description s_("WebexTeamsService|Send notifications about project events to Webex Teams.") end @@ -29,7 +29,7 @@ module Integrations 'webex_teams' end - def help + def self.help docs_link = ActionController::Base.helpers.link_to _('Learn more.'), Rails.application.routes.url_helpers.help_page_url('user/project/integrations/webex_teams'), target: '_blank', rel: 'noopener noreferrer' s_("WebexTeamsService|Send notifications about project events to a Webex Teams conversation. %{docs_link}") % { docs_link: docs_link.html_safe } end diff --git a/app/models/integrations/youtrack.rb b/app/models/integrations/youtrack.rb index 15246a37aa7..932e588a829 100644 --- a/app/models/integrations/youtrack.rb +++ b/app/models/integrations/youtrack.rb @@ -14,15 +14,15 @@ module Integrations @reference_pattern = /(?<issue>\b[A-Za-z][A-Za-z0-9_]*-\d+\b)#{regex_suffix if only_long}/ end - def title + def self.title 'YouTrack' end - def description + def self.description s_("IssueTracker|Use YouTrack as this project's issue tracker.") end - def help + def self.help docs_link = ActionController::Base.helpers.link_to _('Learn more.'), Rails.application.routes.url_helpers.help_page_url('user/project/integrations/youtrack'), target: '_blank', rel: 'noopener noreferrer' s_("IssueTracker|Use YouTrack as this project's issue tracker. %{docs_link}").html_safe % { docs_link: docs_link.html_safe } end diff --git a/app/models/integrations/zentao.rb b/app/models/integrations/zentao.rb index 58ec4abf30c..2aec0c1e871 100644 --- a/app/models/integrations/zentao.rb +++ b/app/models/integrations/zentao.rb @@ -57,18 +57,18 @@ module Integrations data_fields.api_url ||= issues_tracker['api_url'] end - def title + def self.title 'ZenTao' end - def description + def self.description s_("ZentaoIntegration|Use ZenTao as this project's issue tracker.") end - def help + def self.help s_("ZentaoIntegration|Before you enable this integration, you must configure ZenTao. For more details, read the %{link_start}ZenTao integration documentation%{link_end}.") % { link_start: '<a href="%{url}" target="_blank" rel="noopener noreferrer">' - .html_safe % { url: help_page_url('user/project/integrations/zentao') }, + .html_safe % { url: Rails.application.routes.url_helpers.help_page_url('user/project/integrations/zentao') }, link_end: '</a>'.html_safe } end diff --git a/app/serializers/merge_request_noteable_entity.rb b/app/serializers/merge_request_noteable_entity.rb index aac90c20b53..44f51f43998 100644 --- a/app/serializers/merge_request_noteable_entity.rb +++ b/app/serializers/merge_request_noteable_entity.rb @@ -49,10 +49,6 @@ class MergeRequestNoteableEntity < IssuableEntity expose :can_update do |merge_request| can?(current_user, :update_merge_request, merge_request) end - - expose :can_approve do |merge_request| - merge_request.eligible_for_approval_by?(current_user) - end end expose :locked_discussion_docs_path, if: -> (merge_request) { merge_request.discussion_locked? } do |merge_request| diff --git a/app/services/projects/group_links/destroy_service.rb b/app/services/projects/group_links/destroy_service.rb index a2307bfebf0..e0218ae087e 100644 --- a/app/services/projects/group_links/destroy_service.rb +++ b/app/services/projects/group_links/destroy_service.rb @@ -3,8 +3,10 @@ module Projects module GroupLinks class DestroyService < BaseService - def execute(group_link) - return false unless group_link + def execute(group_link, skip_authorization: false) + unless valid_to_destroy?(group_link, skip_authorization) + return ServiceResponse.error(message: 'Not found', reason: :not_found) + end if group_link.project.private? TodosDestroyer::ProjectPrivateWorker.perform_in(Todo::WAIT_FOR_DELETE, project.id) @@ -12,20 +14,29 @@ module Projects TodosDestroyer::ConfidentialIssueWorker.perform_in(Todo::WAIT_FOR_DELETE, nil, project.id) end - group_link.destroy.tap do |link| - refresh_project_authorizations_asynchronously(link.project) + link = group_link.destroy - # Until we compare the inconsistency rates of the new specialized worker and - # the old approach, we still run AuthorizedProjectsWorker - # but with some delay and lower urgency as a safety net. - link.group.refresh_members_authorized_projects( - priority: UserProjectAccessChangedService::LOW_PRIORITY - ) - end + refresh_project_authorizations_asynchronously(link.project) + + # Until we compare the inconsistency rates of the new specialized worker and + # the old approach, we still run AuthorizedProjectsWorker + # but with some delay and lower urgency as a safety net. + link.group.refresh_members_authorized_projects( + priority: UserProjectAccessChangedService::LOW_PRIORITY + ) + + ServiceResponse.success(payload: { link: link }) end private + def valid_to_destroy?(group_link, skip_authorization) + return false unless group_link + return true if skip_authorization + + current_user.can?(:admin_project_group_link, group_link) + end + def refresh_project_authorizations_asynchronously(project) AuthorizedProjectUpdate::ProjectRecalculateWorker.perform_async(project.id) end diff --git a/app/services/projects/group_links/update_service.rb b/app/services/projects/group_links/update_service.rb index 9b2565adaca..04f1552d929 100644 --- a/app/services/projects/group_links/update_service.rb +++ b/app/services/projects/group_links/update_service.rb @@ -10,15 +10,23 @@ module Projects end def execute(group_link_params) + return ServiceResponse.error(message: 'Not found', reason: :not_found) unless allowed_to_update? + group_link.update!(group_link_params) refresh_authorizations if requires_authorization_refresh?(group_link_params) + + ServiceResponse.success end private attr_reader :group_link + def allowed_to_update? + current_user.can?(:admin_project_member, project) + end + def refresh_authorizations AuthorizedProjectUpdate::ProjectRecalculateWorker.perform_async(project.id) diff --git a/app/workers/remove_expired_group_links_worker.rb b/app/workers/remove_expired_group_links_worker.rb index f1da5f37945..0bac595f0c4 100644 --- a/app/workers/remove_expired_group_links_worker.rb +++ b/app/workers/remove_expired_group_links_worker.rb @@ -11,7 +11,7 @@ class RemoveExpiredGroupLinksWorker # rubocop:disable Scalability/IdempotentWork def perform ProjectGroupLink.expired.find_each do |link| - Projects::GroupLinks::DestroyService.new(link.project, nil).execute(link) + Projects::GroupLinks::DestroyService.new(link.project, nil).execute(link, skip_authorization: true) end GroupGroupLink.expired.find_in_batches do |link_batch| |