Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2021-02-17 21:09:19 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2021-02-17 21:09:19 +0300
commitc0ef148ef349f0d13331638ab90f5d9e9d2175ba (patch)
treefd826213aa30339d8fee4bcc1bf35c6e40639823 /app
parentc982bb363b3a0390a274197f410a1609a4667760 (diff)
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
-rw-r--r--app/assets/javascripts/blob/blob_file_dropzone.js4
-rw-r--r--app/assets/javascripts/blob_edit/blob_bundle.js2
-rw-r--r--app/assets/javascripts/commit/pipelines/pipelines_bundle.js1
-rw-r--r--app/assets/javascripts/commit/pipelines/pipelines_table.vue5
-rw-r--r--app/assets/javascripts/experiment_tracking.js25
-rw-r--r--app/assets/javascripts/integrations/edit/components/jira_trigger_fields.vue123
-rw-r--r--app/assets/javascripts/integrations/edit/index.js2
-rw-r--r--app/assets/javascripts/merge_request_tabs.js1
-rw-r--r--app/assets/javascripts/pages/projects/show/index.js7
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/pipeline_url.vue80
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/pipelines.vue5
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table.vue5
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table_row.vue10
-rw-r--r--app/assets/javascripts/pipelines/pipelines_index.js2
-rw-r--r--app/assets/javascripts/projects/upload_file_experiment.js21
-rw-r--r--app/controllers/concerns/comment_and_close_flag.rb2
-rw-r--r--app/controllers/projects_controller.rb2
-rw-r--r--app/finders/packages/npm/package_finder.rb38
-rw-r--r--app/graphql/types/global_id_type.rb5
-rw-r--r--app/helpers/invite_members_helper.rb8
-rw-r--r--app/helpers/services_helper.rb8
-rw-r--r--app/helpers/stat_anchors_helper.rb3
-rw-r--r--app/models/packages/package.rb10
-rw-r--r--app/models/project_services/jira_service.rb74
-rw-r--r--app/presenters/project_presenter.rb61
-rw-r--r--app/views/projects/blob/_upload.html.haml2
-rw-r--r--app/views/projects/commit/_pipelines_list.haml1
-rw-r--r--app/views/projects/empty.html.haml5
-rw-r--r--app/views/projects/pipelines/index.html.haml1
-rw-r--r--app/views/shared/_new_commit_form.html.haml7
-rw-r--r--app/views/shared/issuable/_sidebar_assignees.html.haml3
-rw-r--r--app/workers/post_receive.rb1
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)