diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-02-17 21:09:19 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-02-17 21:09:19 +0300 |
commit | c0ef148ef349f0d13331638ab90f5d9e9d2175ba (patch) | |
tree | fd826213aa30339d8fee4bcc1bf35c6e40639823 /app | |
parent | c982bb363b3a0390a274197f410a1609a4667760 (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
32 files changed, 384 insertions, 140 deletions
diff --git a/app/assets/javascripts/blob/blob_file_dropzone.js b/app/assets/javascripts/blob/blob_file_dropzone.js index e72c5c90986..445602a8765 100644 --- a/app/assets/javascripts/blob/blob_file_dropzone.js +++ b/app/assets/javascripts/blob/blob_file_dropzone.js @@ -3,6 +3,7 @@ import Dropzone from 'dropzone'; import $ from 'jquery'; import { sprintf, __ } from '~/locale'; +import { trackUploadFileFormSubmitted } from '~/projects/upload_file_experiment'; import { HIDDEN_CLASS } from '../lib/utils/constants'; import csrf from '../lib/utils/csrf'; import { visitUrl } from '../lib/utils/url_utility'; @@ -83,6 +84,9 @@ export default class BlobFileDropzone { submitButton.on('click', (e) => { e.preventDefault(); e.stopPropagation(); + + trackUploadFileFormSubmitted(); + if (dropzone[0].dropzone.getQueuedFiles().length === 0) { // eslint-disable-next-line no-alert alert(__('Please select a file')); diff --git a/app/assets/javascripts/blob_edit/blob_bundle.js b/app/assets/javascripts/blob_edit/blob_bundle.js index 173c82ef9b0..6d9b56b4bb8 100644 --- a/app/assets/javascripts/blob_edit/blob_bundle.js +++ b/app/assets/javascripts/blob_edit/blob_bundle.js @@ -4,6 +4,7 @@ import $ from 'jquery'; import initPopover from '~/blob/suggest_gitlab_ci_yml'; import { deprecatedCreateFlash as createFlash } from '~/flash'; import { disableButtonIfEmptyField, setCookie } from '~/lib/utils/common_utils'; +import { initUploadFileTrigger } from '~/projects/upload_file_experiment'; import Tracking from '~/tracking'; import BlobFileDropzone from '../blob/blob_file_dropzone'; import NewCommitForm from '../new_commit_form'; @@ -47,6 +48,7 @@ export const initUploadForm = () => { new NewCommitForm(uploadBlobForm); disableButtonIfEmptyField(uploadBlobForm.find('.js-commit-message'), '.btn-upload-file'); + initUploadFileTrigger(); } }; diff --git a/app/assets/javascripts/commit/pipelines/pipelines_bundle.js b/app/assets/javascripts/commit/pipelines/pipelines_bundle.js index 920ffde3e32..a71bf0d874d 100644 --- a/app/assets/javascripts/commit/pipelines/pipelines_bundle.js +++ b/app/assets/javascripts/commit/pipelines/pipelines_bundle.js @@ -34,7 +34,6 @@ export default () => { helpPagePath: pipelineTableViewEl.dataset.helpPagePath, emptyStateSvgPath: pipelineTableViewEl.dataset.emptyStateSvgPath, errorStateSvgPath: pipelineTableViewEl.dataset.errorStateSvgPath, - autoDevopsHelpPath: pipelineTableViewEl.dataset.helpAutoDevopsPath, }, }); }, diff --git a/app/assets/javascripts/commit/pipelines/pipelines_table.vue b/app/assets/javascripts/commit/pipelines/pipelines_table.vue index 787152d00ef..926f5b82b66 100644 --- a/app/assets/javascripts/commit/pipelines/pipelines_table.vue +++ b/app/assets/javascripts/commit/pipelines/pipelines_table.vue @@ -29,10 +29,6 @@ export default { type: String, required: true, }, - autoDevopsHelpPath: { - type: String, - required: true, - }, errorStateSvgPath: { type: String, required: true, @@ -212,7 +208,6 @@ export default { <pipelines-table-component :pipelines="state.pipelines" :update-graph-dropdown="updateGraphDropdown" - :auto-devops-help-path="autoDevopsHelpPath" :view-type="viewType" > <template #table-header-actions> diff --git a/app/assets/javascripts/experiment_tracking.js b/app/assets/javascripts/experiment_tracking.js new file mode 100644 index 00000000000..4611af3f857 --- /dev/null +++ b/app/assets/javascripts/experiment_tracking.js @@ -0,0 +1,25 @@ +import { get } from 'lodash'; +import Tracking from '~/tracking'; + +const TRACKING_CONTEXT_SCHEMA = 'iglu:com.gitlab/gitlab_experiment/jsonschema/1-0-0'; + +export default class ExperimentTracking { + constructor(experimentName, { label } = {}) { + this.label = label; + this.experimentData = get(window, ['gon', 'global', 'experiment', experimentName]); + } + + event(action) { + if (!this.experimentData) { + return false; + } + + return Tracking.event(document.body.dataset.page, action, { + label: this.label, + context: { + schema: TRACKING_CONTEXT_SCHEMA, + data: this.experimentData, + }, + }); + } +} diff --git a/app/assets/javascripts/integrations/edit/components/jira_trigger_fields.vue b/app/assets/javascripts/integrations/edit/components/jira_trigger_fields.vue index af4e9acf4ba..22a767cfaae 100644 --- a/app/assets/javascripts/integrations/edit/components/jira_trigger_fields.vue +++ b/app/assets/javascripts/integrations/edit/components/jira_trigger_fields.vue @@ -1,7 +1,16 @@ <script> -import { GlFormGroup, GlFormCheckbox, GlFormRadio } from '@gitlab/ui'; +import { + GlFormGroup, + GlFormCheckbox, + GlFormRadio, + GlFormInput, + GlLink, + GlSprintf, +} from '@gitlab/ui'; import { mapGetters } from 'vuex'; +import { helpPagePath } from '~/helpers/help_page_helper'; import { s__ } from '~/locale'; +import eventHub from '../event_hub'; const commentDetailOptions = [ { @@ -18,12 +27,41 @@ const commentDetailOptions = [ }, ]; +const ISSUE_TRANSITION_AUTO = 'auto'; +const ISSUE_TRANSITION_CUSTOM = 'custom'; + +const issueTransitionOptions = [ + { + value: ISSUE_TRANSITION_AUTO, + label: s__('JiraService|Move to Done'), + help: s__( + 'JiraService|Automatically transitions Jira issues to the "Done" category. %{linkStart}Learn more%{linkEnd}', + ), + link: helpPagePath('user/project/integrations/jira.html', { + anchor: 'automatic-issue-transitions', + }), + }, + { + value: ISSUE_TRANSITION_CUSTOM, + label: s__('JiraService|Use custom transitions'), + help: s__( + 'JiraService|Set a custom final state by using transition IDs. %{linkStart}Learn about transition IDs%{linkEnd}', + ), + link: helpPagePath('user/project/integrations/jira.html', { + anchor: 'custom-issue-transitions', + }), + }, +]; + export default { name: 'JiraTriggerFields', components: { GlFormGroup, GlFormCheckbox, GlFormRadio, + GlFormInput, + GlLink, + GlSprintf, }, props: { initialTriggerCommit: { @@ -43,21 +81,52 @@ export default { required: false, default: 'standard', }, + initialJiraIssueTransitionId: { + type: String, + required: false, + default: '', + }, }, data() { return { + validated: false, triggerCommit: this.initialTriggerCommit, triggerMergeRequest: this.initialTriggerMergeRequest, enableComments: this.initialEnableComments, commentDetail: this.initialCommentDetail, + jiraIssueTransitionId: this.initialJiraIssueTransitionId, + issueTransitionMode: this.initialJiraIssueTransitionId.length + ? ISSUE_TRANSITION_CUSTOM + : ISSUE_TRANSITION_AUTO, commentDetailOptions, + issueTransitionOptions, }; }, computed: { ...mapGetters(['isInheriting']), - showEnableComments() { + showTriggerSettings() { return this.triggerCommit || this.triggerMergeRequest; }, + validIssueTransitionId() { + return !this.validated || this.jiraIssueTransitionId.length > 0; + }, + }, + created() { + eventHub.$on('validateForm', this.validateForm); + }, + beforeDestroy() { + eventHub.$off('validateForm', this.validateForm); + }, + methods: { + validateForm() { + this.validated = true; + }, + showCustomIssueTransitions(currentOption) { + return ( + this.issueTransitionMode === ISSUE_TRANSITION_CUSTOM && + currentOption === ISSUE_TRANSITION_CUSTOM + ); + }, }, }; </script> @@ -89,7 +158,7 @@ export default { </gl-form-group> <gl-form-group - v-show="showEnableComments" + v-show="showTriggerSettings" :label="s__('Integrations|Comment settings:')" label-for="service[comment_on_event_enabled]" class="gl-pl-6" @@ -106,7 +175,7 @@ export default { </gl-form-group> <gl-form-group - v-show="showEnableComments && enableComments" + v-show="showTriggerSettings && enableComments" :label="s__('Integrations|Comment detail:')" label-for="service[comment_detail]" class="gl-pl-9" @@ -126,5 +195,51 @@ export default { </template> </gl-form-radio> </gl-form-group> + + <gl-form-group + v-show="showTriggerSettings" + :label="s__('JiraService|Transition Jira issues to their final state:')" + class="gl-pl-6" + data-testid="issue-transition-settings" + > + <input type="hidden" name="service[jira_issue_transition_id]" value="" /> + + <gl-form-radio + v-for="issueTransitionOption in issueTransitionOptions" + :key="issueTransitionOption.value" + v-model="issueTransitionMode" + :value="issueTransitionOption.value" + :disabled="isInheriting" + :data-qa-selector="`service_issue_transition_mode_${issueTransitionOption.value}`" + > + {{ issueTransitionOption.label }} + + <template v-if="showCustomIssueTransitions(issueTransitionOption.value)"> + <gl-form-input + v-model="jiraIssueTransitionId" + name="service[jira_issue_transition_id]" + type="text" + class="gl-my-3" + data-qa-selector="service_jira_issue_transition_id_field" + :placeholder="s__('JiraService|For example, 12, 24')" + :disabled="isInheriting" + :required="true" + :state="validIssueTransitionId" + /> + + <span class="invalid-feedback"> + {{ s__('This field is required.') }} + </span> + </template> + + <template #help> + <gl-sprintf :message="issueTransitionOption.help"> + <template #link="{ content }"> + <gl-link :href="issueTransitionOption.link" target="_blank">{{ content }}</gl-link> + </template> + </gl-sprintf> + </template> + </gl-form-radio> + </gl-form-group> </div> </template> diff --git a/app/assets/javascripts/integrations/edit/index.js b/app/assets/javascripts/integrations/edit/index.js index ab9bdd9ca2e..1ae353ab6e3 100644 --- a/app/assets/javascripts/integrations/edit/index.js +++ b/app/assets/javascripts/integrations/edit/index.js @@ -28,6 +28,7 @@ function parseDatasetToProps(data) { testPath, resetPath, vulnerabilitiesIssuetype, + jiraIssueTransitionId, ...booleanAttributes } = data; const { @@ -59,6 +60,7 @@ function parseDatasetToProps(data) { initialTriggerMergeRequest: mergeRequestEvents, initialEnableComments: enableComments, initialCommentDetail: commentDetail, + initialJiraIssueTransitionId: jiraIssueTransitionId, }, jiraIssuesProps: { showJiraIssuesIntegration, diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js index 4dab796d8a4..64c49b4276e 100644 --- a/app/assets/javascripts/merge_request_tabs.js +++ b/app/assets/javascripts/merge_request_tabs.js @@ -364,7 +364,6 @@ export default class MergeRequestTabs { helpPagePath: pipelineTableViewEl.dataset.helpPagePath, emptyStateSvgPath: pipelineTableViewEl.dataset.emptyStateSvgPath, errorStateSvgPath: pipelineTableViewEl.dataset.errorStateSvgPath, - autoDevopsHelpPath: pipelineTableViewEl.dataset.helpAutoDevopsPath, canCreatePipelineInTargetProject: Boolean( mrWidgetData?.can_create_pipeline_in_target_project, ), diff --git a/app/assets/javascripts/pages/projects/show/index.js b/app/assets/javascripts/pages/projects/show/index.js index 0494dad6e33..e5ec9976ac5 100644 --- a/app/assets/javascripts/pages/projects/show/index.js +++ b/app/assets/javascripts/pages/projects/show/index.js @@ -24,9 +24,12 @@ new UserCallout({ }); // Project show page loads different overview content based on user preferences -const treeSlider = document.getElementById('js-tree-list'); -if (treeSlider) { + +if (document.querySelector('.js-upload-blob-form')) { initUploadForm(); +} + +if (document.getElementById('js-tree-list')) { initTree(); } diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_url.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_url.vue index 823ada133d2..a61ffe8f0fc 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_url.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_url.vue @@ -1,5 +1,6 @@ <script> import { GlLink, GlPopover, GlSprintf, GlTooltipDirective, GlBadge } from '@gitlab/ui'; +import { helpPagePath } from '~/helpers/help_page_helper'; import { SCHEDULE_ORIGIN } from '../../constants'; export default { @@ -26,10 +27,6 @@ export default { type: String, required: true, }, - autoDevopsHelpPath: { - type: String, - required: true, - }, }, computed: { user() { @@ -44,6 +41,12 @@ export default { this.pipeline?.project?.full_path !== `/${this.targetProjectFullPath}`, ); }, + autoDevopsTagId() { + return `pipeline-url-autodevops-${this.pipeline.id}`; + }, + autoDevopsHelpPath() { + return helpPagePath('topics/autodevops/index.md'); + }, }, }; </script> @@ -103,38 +106,43 @@ export default { data-testid="pipeline-url-failure" >{{ __('error') }}</gl-badge > - <gl-link - v-if="pipeline.flags.auto_devops" - :id="`pipeline-url-autodevops-${pipeline.id}`" - tabindex="0" - data-testid="pipeline-url-autodevops" - role="button" - ><gl-badge variant="info" size="sm">{{ __('Auto DevOps') }}</gl-badge></gl-link - > - <gl-popover - :target="`pipeline-url-autodevops-${pipeline.id}`" - triggers="focus" - placement="top" - > - <template #title> - <div class="gl-font-weight-normal gl-line-height-normal"> - <gl-sprintf - :message=" - __( - 'This pipeline makes use of a predefined CI/CD configuration enabled by %{strongStart}Auto DevOps.%{strongEnd}', - ) - " - > - <template #strong="{ content }"> - <b>{{ content }}</b> - </template> - </gl-sprintf> - </div> - </template> - <gl-link :href="autoDevopsHelpPath" target="_blank" rel="noopener noreferrer nofollow">{{ - __('Learn more about Auto DevOps') - }}</gl-link> - </gl-popover> + <template v-if="pipeline.flags.auto_devops"> + <gl-link + :id="autoDevopsTagId" + tabindex="0" + data-testid="pipeline-url-autodevops" + role="button" + > + <gl-badge variant="info" size="sm"> + {{ __('Auto DevOps') }} + </gl-badge> + </gl-link> + <gl-popover :target="autoDevopsTagId" triggers="focus" placement="top"> + <template #title> + <div class="gl-font-weight-normal gl-line-height-normal"> + <gl-sprintf + :message=" + __( + 'This pipeline makes use of a predefined CI/CD configuration enabled by %{strongStart}Auto DevOps.%{strongEnd}', + ) + " + > + <template #strong="{ content }"> + <b>{{ content }}</b> + </template> + </gl-sprintf> + </div> + </template> + <gl-link + :href="autoDevopsHelpPath" + data-testid="pipeline-url-autodevops-link" + target="_blank" + > + {{ __('Learn more about Auto DevOps') }} + </gl-link> + </gl-popover> + </template> + <gl-badge v-if="pipeline.flags.stuck" variant="warning" diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines.vue index 48009a9fcb8..30a099fd1c7 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines.vue @@ -68,10 +68,6 @@ export default { type: String, required: true, }, - autoDevopsHelpPath: { - type: String, - required: true, - }, hasGitlabCi: { type: Boolean, required: true, @@ -362,7 +358,6 @@ export default { :pipelines="state.pipelines" :pipeline-schedule-url="pipelineScheduleUrl" :update-graph-dropdown="updateGraphDropdown" - :auto-devops-help-path="autoDevopsHelpPath" :view-type="viewType" /> </div> diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table.vue index 24c67184e56..fdc8c3e1866 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table.vue @@ -32,10 +32,6 @@ export default { required: false, default: false, }, - autoDevopsHelpPath: { - type: String, - required: true, - }, viewType: { type: String, required: true, @@ -102,7 +98,6 @@ export default { :pipeline="model" :pipeline-schedule-url="pipelineScheduleUrl" :update-graph-dropdown="updateGraphDropdown" - :auto-devops-help-path="autoDevopsHelpPath" :view-type="viewType" :canceling-pipeline="cancelingPipeline" /> diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table_row.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table_row.vue index 572abe2a24a..68deca313eb 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table_row.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table_row.vue @@ -47,10 +47,6 @@ export default { required: false, default: false, }, - autoDevopsHelpPath: { - type: String, - required: true, - }, viewType: { type: String, required: true, @@ -194,11 +190,7 @@ export default { </div> </div> - <pipeline-url - :pipeline="pipeline" - :pipeline-schedule-url="pipelineScheduleUrl" - :auto-devops-help-path="autoDevopsHelpPath" - /> + <pipeline-url :pipeline="pipeline" :pipeline-schedule-url="pipelineScheduleUrl" /> <pipeline-triggerer :pipeline="pipeline" /> <div class="table-section section-wrap section-20"> diff --git a/app/assets/javascripts/pipelines/pipelines_index.js b/app/assets/javascripts/pipelines/pipelines_index.js index 7bcc51e18e5..a7dea2cf103 100644 --- a/app/assets/javascripts/pipelines/pipelines_index.js +++ b/app/assets/javascripts/pipelines/pipelines_index.js @@ -27,7 +27,6 @@ export const initPipelinesIndex = (selector = '#pipelines-list-vue') => { emptyStateSvgPath, errorStateSvgPath, noPipelinesSvgPath, - autoDevopsHelpPath, newPipelinePath, canCreatePipeline, hasGitlabCi, @@ -60,7 +59,6 @@ export const initPipelinesIndex = (selector = '#pipelines-list-vue') => { emptyStateSvgPath, errorStateSvgPath, noPipelinesSvgPath, - autoDevopsHelpPath, newPipelinePath, canCreatePipeline: parseBoolean(canCreatePipeline), hasGitlabCi: parseBoolean(hasGitlabCi), diff --git a/app/assets/javascripts/projects/upload_file_experiment.js b/app/assets/javascripts/projects/upload_file_experiment.js new file mode 100644 index 00000000000..c2a68043489 --- /dev/null +++ b/app/assets/javascripts/projects/upload_file_experiment.js @@ -0,0 +1,21 @@ +import ExperimentTracking from '~/experiment_tracking'; + +function trackEvent(eventName) { + const Tracking = new ExperimentTracking('empty_repo_upload', { label: 'blob-upload-modal' }); + + Tracking.event(eventName); +} + +export function initUploadFileTrigger() { + const uploadFileTriggerEl = document.querySelector('.js-upload-file-experiment-trigger'); + + if (uploadFileTriggerEl) { + uploadFileTriggerEl.addEventListener('click', () => { + trackEvent('click_upload_modal_trigger'); + }); + } +} + +export function trackUploadFileFormSubmitted() { + trackEvent('click_upload_modal_form_submit'); +} diff --git a/app/controllers/concerns/comment_and_close_flag.rb b/app/controllers/concerns/comment_and_close_flag.rb index e2f3272abbc..02a68d2857c 100644 --- a/app/controllers/concerns/comment_and_close_flag.rb +++ b/app/controllers/concerns/comment_and_close_flag.rb @@ -5,7 +5,7 @@ module CommentAndCloseFlag included do before_action do - push_frontend_feature_flag(:remove_comment_close_reopen, @group) + push_frontend_feature_flag(:remove_comment_close_reopen, @group || @project&.group) end end end diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index ebffb62cff3..d6ba14f4f7f 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -329,6 +329,8 @@ class ProjectsController < Projects::ApplicationController if can?(current_user, :download_code, @project) return render 'projects/no_repo' unless @project.repository_exists? + experiment(:empty_repo_upload, project: @project).track(:view_project_show) if @project.can_current_user_push_to_default_branch? + if @project.empty_repo? record_experiment_user(:invite_members_empty_project_version_a) diff --git a/app/finders/packages/npm/package_finder.rb b/app/finders/packages/npm/package_finder.rb index 2854226e178..04b124ddd83 100644 --- a/app/finders/packages/npm/package_finder.rb +++ b/app/finders/packages/npm/package_finder.rb @@ -2,29 +2,41 @@ module Packages module Npm class PackageFinder - attr_reader :project, :package_name - delegate :find_by_version, to: :execute + delegate :last, to: :execute - def initialize(project, package_name) - @project = project + def initialize(package_name, project: nil, namespace: nil) @package_name = package_name + @project = project + @namespace = namespace end def execute - return Packages::Package.none unless project - - packages + base.npm + .with_name(@package_name) + .last_of_each_version + .preload_files end private - def packages - project.packages - .npm - .with_name(package_name) - .last_of_each_version - .preload_files + def base + if @project + packages_for_project + elsif @namespace + packages_for_namespace + else + ::Packages::Package.none + end + end + + def packages_for_project + @project.packages + end + + def packages_for_namespace + projects = ::Project.in_namespace(@namespace.self_and_descendants.select(:id)) + ::Packages::Package.for_projects(projects.select(:id)) end end end diff --git a/app/graphql/types/global_id_type.rb b/app/graphql/types/global_id_type.rb index ed28c3ffd7e..007d86f60b8 100644 --- a/app/graphql/types/global_id_type.rb +++ b/app/graphql/types/global_id_type.rb @@ -67,7 +67,10 @@ module Types end self.define_singleton_method(:suitable?) do |gid| - gid&.model_class&.ancestors&.include?(model_class) + next false if gid.nil? + + gid.model_name.safe_constantize.present? && + gid.model_class.ancestors.include?(model_class) end self.define_singleton_method(:coerce_input) do |string, ctx| diff --git a/app/helpers/invite_members_helper.rb b/app/helpers/invite_members_helper.rb index 889365e39de..961ef613a06 100644 --- a/app/helpers/invite_members_helper.rb +++ b/app/helpers/invite_members_helper.rb @@ -23,6 +23,14 @@ module InviteMembersHelper end end + def show_invite_members_track_event + if directly_invite_members? + 'show_invite_members' + elsif indirectly_invite_members? + 'show_invite_members_version_b' + end + end + def invite_group_members?(group) experiment_enabled?(:invite_members_empty_group_version_a) && Ability.allowed?(current_user, :admin_group_member, group) end diff --git a/app/helpers/services_helper.rb b/app/helpers/services_helper.rb index 14d20e7c622..8fc63dcdb3a 100644 --- a/app/helpers/services_helper.rb +++ b/app/helpers/services_helper.rb @@ -86,7 +86,7 @@ module ServicesHelper end def integration_form_data(integration, group: nil) - { + form_data = { id: integration.id, show_active: integration.show_active_box?.to_s, activated: (integration.active || integration.new_record?).to_s, @@ -106,6 +106,12 @@ module ServicesHelper test_path: scoped_test_integration_path(integration), reset_path: scoped_reset_integration_path(integration, group: group) } + + if integration.is_a?(JiraService) + form_data[:jira_issue_transition_id] = integration.jira_issue_transition_id + end + + form_data end def trigger_events_for_service(integration) diff --git a/app/helpers/stat_anchors_helper.rb b/app/helpers/stat_anchors_helper.rb index 1e8e6371284..d9429f28be7 100644 --- a/app/helpers/stat_anchors_helper.rb +++ b/app/helpers/stat_anchors_helper.rb @@ -5,13 +5,14 @@ module StatAnchorsHelper {}.tap do |attrs| attrs[:class] = %w(nav-link gl-display-flex gl-align-items-center) << extra_classes(anchor) attrs[:itemprop] = anchor.itemprop if anchor.itemprop + attrs[:data] = anchor.data if anchor.data end end private def button_attribute(anchor) - "btn-#{anchor.class_modifier || 'dashed'}" + anchor.class_modifier || 'btn-dashed' end def extra_classes(anchor) diff --git a/app/models/packages/package.rb b/app/models/packages/package.rb index 391540634be..0673edfd831 100644 --- a/app/models/packages/package.rb +++ b/app/models/packages/package.rb @@ -40,11 +40,11 @@ class Packages::Package < ApplicationRecord validate :unique_debian_package_name, if: :debian_package? validate :valid_conan_package_recipe, if: :conan? - validate :valid_npm_package_name, if: :npm? validate :valid_composer_global_name, if: :composer? validate :package_already_taken, if: :npm? validates :name, format: { with: Gitlab::Regex.conan_recipe_component_regex }, if: :conan? validates :name, format: { with: Gitlab::Regex.generic_package_name_regex }, if: :generic? + validates :name, format: { with: Gitlab::Regex.npm_package_name_regex }, if: :npm? validates :name, format: { with: Gitlab::Regex.nuget_package_name_regex }, if: :nuget? validates :name, format: { with: Gitlab::Regex.debian_package_name_regex }, if: :debian_package? validates :name, inclusion: { in: %w[incoming] }, if: :debian_incoming? @@ -247,14 +247,6 @@ class Packages::Package < ApplicationRecord end end - def valid_npm_package_name - return unless project&.root_namespace - - unless name =~ %r{\A@#{project.root_namespace.path}/[^/]+\z} - errors.add(:name, 'is not valid') - end - end - def package_already_taken return unless project diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb index 5857d86f921..12f7bac23e4 100644 --- a/app/models/project_services/jira_service.rb +++ b/app/models/project_services/jira_service.rb @@ -124,15 +124,11 @@ class JiraService < IssueTrackerService end def fields - transition_id_help_path = help_page_path('user/project/integrations/jira', anchor: 'obtaining-a-transition-id') - transition_id_help_link_start = '<a href="%{transition_id_help_path}" target="_blank" rel="noopener noreferrer">'.html_safe % { transition_id_help_path: transition_id_help_path } - [ { type: 'text', name: 'url', title: s_('JiraService|Web URL'), placeholder: 'https://jira.example.com', required: true }, { type: 'text', name: 'api_url', title: s_('JiraService|Jira API URL'), placeholder: s_('JiraService|If different from Web URL') }, { type: 'text', name: 'username', title: s_('JiraService|Username or Email'), placeholder: s_('JiraService|Use a username for server version and an email for cloud version'), required: true }, - { type: 'password', name: 'password', title: s_('JiraService|Password or API token'), placeholder: s_('JiraService|Use a password for server version and an API token for cloud version'), required: true }, - { type: 'text', name: 'jira_issue_transition_id', title: s_('JiraService|Jira workflow transition IDs'), placeholder: s_('JiraService|For example, 12, 24'), help: s_('JiraService|Set transition IDs for Jira workflow transitions. %{link_start}Learn more%{link_end}'.html_safe % { link_start: transition_id_help_link_start, link_end: '</a>'.html_safe }) } + { type: 'password', name: 'password', title: s_('JiraService|Password or API token'), placeholder: s_('JiraService|Use a password for server version and an API token for cloud version'), required: true } ] end @@ -159,17 +155,19 @@ class JiraService < IssueTrackerService # support any events. end - def find_issue(issue_key, rendered_fields: false) - options = {} - options = options.merge(expand: 'renderedFields') if rendered_fields + def find_issue(issue_key, rendered_fields: false, transitions: false) + expands = [] + expands << 'renderedFields' if rendered_fields + expands << 'transitions' if transitions + options = { expand: expands.join(',') } if expands.any? - jira_request { client.Issue.find(issue_key, options) } + jira_request { client.Issue.find(issue_key, options || {}) } end def close_issue(entity, external_issue, current_user) - issue = find_issue(external_issue.iid) + issue = find_issue(external_issue.iid, transitions: automatic_issue_transitions?) - return if issue.nil? || has_resolution?(issue) || !jira_issue_transition_id.present? + return if issue.nil? || has_resolution?(issue) commit_id = case entity when Commit then entity.id @@ -260,24 +258,52 @@ class JiraService < IssueTrackerService end end + def automatic_issue_transitions? + jira_issue_transition_id.blank? + end + # jira_issue_transition_id can have multiple values split by , or ; # the issue is transitioned at the order given by the user # if any transition fails it will log the error message and stop the transition sequence def transition_issue(issue) - jira_issue_transition_id.scan(Gitlab::Regex.jira_transition_id_regex).each do |transition_id| - issue.transitions.build.save!(transition: { id: transition_id }) - rescue => error - log_error( - "Issue transition failed", - error: { - exception_class: error.class.name, - exception_message: error.message, - exception_backtrace: Gitlab::BacktraceCleaner.clean_backtrace(error.backtrace) - }, - client_url: client_url - ) - return false + return transition_issue_to_done(issue) if automatic_issue_transitions? + + jira_issue_transition_id.scan(Gitlab::Regex.jira_transition_id_regex).all? do |transition_id| + transition_issue_to_id(issue, transition_id) + end + end + + def transition_issue_to_id(issue, transition_id) + issue.transitions.build.save!( + transition: { id: transition_id } + ) + + true + rescue => error + log_error( + "Issue transition failed", + error: { + exception_class: error.class.name, + exception_message: error.message, + exception_backtrace: Gitlab::BacktraceCleaner.clean_backtrace(error.backtrace) + }, + client_url: client_url + ) + + false + end + + def transition_issue_to_done(issue) + transitions = issue.transitions rescue [] + + transition = transitions.find do |transition| + status = transition&.to&.statusCategory + status && status['key'] == 'done' end + + return false unless transition + + transition_issue_to_id(issue, transition.id) end def log_usage(action, user) diff --git a/app/presenters/project_presenter.rb b/app/presenters/project_presenter.rb index e13ef7a3811..a2d25e425da 100644 --- a/app/presenters/project_presenter.rb +++ b/app/presenters/project_presenter.rb @@ -10,10 +10,11 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated include BlobHelper include ChecksCollaboration include Gitlab::Utils::StrongMemoize + include Gitlab::Experiment::Dsl presents :project - AnchorData = Struct.new(:is_link, :label, :link, :class_modifier, :icon, :itemprop) + AnchorData = Struct.new(:is_link, :label, :link, :class_modifier, :icon, :itemprop, :data) MAX_TOPICS_TO_SHOW = 3 def statistic_icon(icon_name = 'plus-square-o') @@ -33,6 +34,7 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated def statistics_buttons(show_auto_devops_callout:) [ + upload_anchor_data, readme_anchor_data, license_anchor_data, changelog_anchor_data, @@ -49,6 +51,7 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated def empty_repo_statistics_buttons [ + upload_anchor_data, new_file_anchor_data, readme_anchor_data, license_anchor_data, @@ -154,6 +157,8 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated end def can_current_user_push_to_branch?(branch) + return false unless current_user + user_access(project).can_push_to_branch?(branch) end @@ -232,19 +237,47 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated empty_repo? ? nil : project_tags_path(project)) end + def upload_anchor_data + strong_memoize(:upload_anchor_data) do + next unless can_current_user_push_to_default_branch? + + experiment(:empty_repo_upload, project: project) do |e| + e.use {} + e.try do + AnchorData.new(false, + statistic_icon('upload') + _('Upload file'), + '#modal-upload-blob', + 'js-upload-file-experiment-trigger', + nil, + nil, + { + 'toggle' => 'modal', + 'target' => '#modal-upload-blob' + } + ) + end + e.run + end + end + end + + def empty_repo_upload_experiment? + upload_anchor_data.present? + end + def new_file_anchor_data - if current_user && can_current_user_push_to_default_branch? + if can_current_user_push_to_default_branch? new_file_path = empty_repo? ? ide_edit_path(project, default_branch_or_master) : project_new_blob_path(project, default_branch_or_master) AnchorData.new(false, statistic_icon + _('New file'), new_file_path, - 'dashed') + 'btn-dashed') end end def readme_anchor_data - if current_user && can_current_user_push_to_default_branch? && readme_path.nil? + if can_current_user_push_to_default_branch? && readme_path.nil? AnchorData.new(false, statistic_icon + _('Add README'), empty_repo? ? add_readme_ide_path : add_readme_path) @@ -252,13 +285,13 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated AnchorData.new(false, statistic_icon('doc-text') + _('README'), default_view != 'readme' ? readme_path : '#readme', - 'default', + 'btn-default', 'doc-text') end end def changelog_anchor_data - if current_user && can_current_user_push_to_default_branch? && repository.changelog.blank? + if can_current_user_push_to_default_branch? && repository.changelog.blank? AnchorData.new(false, statistic_icon + _('Add CHANGELOG'), empty_repo? ? add_changelog_ide_path : add_changelog_path) @@ -266,7 +299,7 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated AnchorData.new(false, statistic_icon('doc-text') + _('CHANGELOG'), changelog_path, - 'default') + 'btn-default') end end @@ -277,11 +310,11 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated AnchorData.new(false, icon + content_tag(:span, license_short_name, class: 'project-stat-value'), license_path, - 'default', + 'btn-default', nil, 'license') else - if current_user && can_current_user_push_to_default_branch? + if can_current_user_push_to_default_branch? AnchorData.new(false, content_tag(:span, statistic_icon + _('Add LICENSE'), class: 'add-license-link d-flex'), empty_repo? ? add_license_ide_path : add_license_path) @@ -294,7 +327,7 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated end def contribution_guide_anchor_data - if current_user && can_current_user_push_to_default_branch? && repository.contribution_guide.blank? + if can_current_user_push_to_default_branch? && repository.contribution_guide.blank? AnchorData.new(false, statistic_icon + _('Add CONTRIBUTING'), empty_repo? ? add_contribution_guide_ide_path : add_contribution_guide_path) @@ -302,7 +335,7 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated AnchorData.new(false, statistic_icon('doc-text') + _('CONTRIBUTING'), contribution_guide_path, - 'default') + 'btn-default') end end @@ -312,7 +345,7 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated AnchorData.new(false, statistic_icon('settings') + _('Auto DevOps enabled'), project_settings_ci_cd_path(project, anchor: 'autodevops-settings'), - 'default') + 'btn-default') else AnchorData.new(false, statistic_icon + _('Enable Auto DevOps'), @@ -337,7 +370,7 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated AnchorData.new(false, _('Kubernetes'), cluster_link, - 'default') + 'btn-default') end end end @@ -351,7 +384,7 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated AnchorData.new(false, statistic_icon('doc-text') + _('CI/CD configuration'), ci_configuration_path, - 'default') + 'btn-default') end end diff --git a/app/views/projects/blob/_upload.html.haml b/app/views/projects/blob/_upload.html.haml index f400c7de5eb..b68c75701b9 100644 --- a/app/views/projects/blob/_upload.html.haml +++ b/app/views/projects/blob/_upload.html.haml @@ -17,7 +17,7 @@ %br .dropzone-alerts.gl-alert.gl-alert-danger.gl-mb-5.data{ style: "display:none" } - = render 'shared/new_commit_form', placeholder: placeholder + = render 'shared/new_commit_form', placeholder: placeholder, ref: local_assigns[:ref] .form-actions = button_tag class: 'btn gl-button btn-success btn-upload-file', id: 'submit-all', type: 'button' do diff --git a/app/views/projects/commit/_pipelines_list.haml b/app/views/projects/commit/_pipelines_list.haml index 81c354f1c8f..92867fdf349 100644 --- a/app/views/projects/commit/_pipelines_list.haml +++ b/app/views/projects/commit/_pipelines_list.haml @@ -2,7 +2,6 @@ #commit-pipeline-table-view{ data: { disable_initialization: disable_initialization, endpoint: endpoint, "help-page-path" => help_page_path('ci/quick_start/README'), - "help-auto-devops-path" => help_page_path('topics/autodevops/index.md'), "empty-state-svg-path" => image_path('illustrations/pipelines_empty.svg'), "error-state-svg-path" => image_path('illustrations/pipelines_failed.svg'), "project-id": @project.id, diff --git a/app/views/projects/empty.html.haml b/app/views/projects/empty.html.haml index 2c245c1a914..d16f271c8d8 100644 --- a/app/views/projects/empty.html.haml +++ b/app/views/projects/empty.html.haml @@ -1,5 +1,5 @@ - @content_class = "limit-container-width" unless fluid_layout -- default_branch_name = @project.default_branch || "master" +- default_branch_name = @project.default_branch_or_master - @skip_current_level_breadcrumb = true = content_for :invite_members_sidebar do @@ -77,3 +77,6 @@ %span>< git push -u origin --all git push -u origin --tags + +- if @project.empty_repo_upload_experiment? + = render 'projects/blob/upload', title: _('Upload New File'), placeholder: _('Upload New File'), button_title: _('Upload file'), form_path: project_create_blob_path(@project, default_branch_name), ref: default_branch_name, method: :post diff --git a/app/views/projects/pipelines/index.html.haml b/app/views/projects/pipelines/index.html.haml index 6a4dd88ae07..70d67fa75b7 100644 --- a/app/views/projects/pipelines/index.html.haml +++ b/app/views/projects/pipelines/index.html.haml @@ -8,7 +8,6 @@ project_id: @project.id, params: params.to_json, "help-page-path" => help_page_path('ci/quick_start/README'), - "auto-devops-help-path" => help_page_path('topics/autodevops/index.md'), "pipeline-schedule-url" => pipeline_schedules_path(@project), "empty-state-svg-path" => image_path('illustrations/pipelines_empty.svg'), "error-state-svg-path" => image_path('illustrations/pipelines_failed.svg'), diff --git a/app/views/shared/_new_commit_form.html.haml b/app/views/shared/_new_commit_form.html.haml index 62ba89e2576..5641c67e462 100644 --- a/app/views/shared/_new_commit_form.html.haml +++ b/app/views/shared/_new_commit_form.html.haml @@ -3,8 +3,11 @@ = render 'shared/commit_message_container', placeholder: placeholder -- if @project.empty_repo? - = hidden_field_tag 'branch_name', @ref +- if project.empty_repo? + - ref = local_assigns[:ref] || @ref + - branch_name_class = project.empty_repo_upload_experiment? ? 'js-branch-name' : nil + + = hidden_field_tag 'branch_name', ref, class: branch_name_class - else - if can?(current_user, :push_code, @project) .form-group.row.branch diff --git a/app/views/shared/issuable/_sidebar_assignees.html.haml b/app/views/shared/issuable/_sidebar_assignees.html.haml index 2b6920ed80f..26986c913f0 100644 --- a/app/views/shared/issuable/_sidebar_assignees.html.haml +++ b/app/views/shared/issuable/_sidebar_assignees.html.haml @@ -43,6 +43,9 @@ - options[:dropdown_class] += ' dropdown-extended-height' - options[:footer_content] = true - options[:wrapper_class] = 'js-sidebar-assignee-dropdown' + - options[:toggle_class] += ' js-invite-members-track' + - data['track-event'] = show_invite_members_track_event + - options[:data].merge!(data) - invite_text = _('Invite Members') - track_label = 'edit_assignee' diff --git a/app/workers/post_receive.rb b/app/workers/post_receive.rb index ac55f883fc5..313b901c08e 100644 --- a/app/workers/post_receive.rb +++ b/app/workers/post_receive.rb @@ -123,6 +123,7 @@ class PostReceive # rubocop:disable Scalability/IdempotentWorker def after_project_changes_hooks(project, user, refs, changes) experiment(:new_project_readme, actor: user).track_initial_writes(project) + experiment(:empty_repo_upload, project: project).track(:initial_write) if project.empty_repo? repository_update_hook_data = Gitlab::DataBuilder::Repository.update(project, user, changes, refs) SystemHooksService.new.execute_hooks(repository_update_hook_data, :repository_update_hooks) Gitlab::UsageDataCounters::SourceCodeCounter.count(:pushes) |