summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2021-03-25 12:09:19 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2021-03-25 12:09:19 +0000
commita156fc95eb8499fec9cd081d30629f0faf18bfe9 (patch)
treeff59d44794ba9d8084e4d59057ec9507b3ba8e2f /app
parent618be8f52d6349533c709a1d702e45b84338c36a (diff)
downloadgitlab-ce-a156fc95eb8499fec9cd081d30629f0faf18bfe9.tar.gz
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
-rw-r--r--app/assets/javascripts/diffs/components/diff_row.vue15
-rw-r--r--app/assets/javascripts/integrations/edit/components/dynamic_field.vue11
-rw-r--r--app/assets/javascripts/integrations/edit/index.js5
-rw-r--r--app/assets/javascripts/issuable_bulk_update_actions.js4
-rw-r--r--app/assets/javascripts/issuable_bulk_update_sidebar.js22
-rw-r--r--app/assets/javascripts/issuable_index.js2
-rw-r--r--app/assets/javascripts/issuable_list/components/issuable_item.vue6
-rw-r--r--app/assets/javascripts/issuable_list/components/issuable_list_root.vue2
-rw-r--r--app/assets/javascripts/issues_list/components/issues_list_app.vue18
-rw-r--r--app/assets/javascripts/labels_select.js8
-rw-r--r--app/assets/javascripts/pages/projects/issues/index/index.js7
-rw-r--r--app/assets/stylesheets/pages/notes.scss20
-rw-r--r--app/controllers/projects/issues_controller.rb1
-rw-r--r--app/experiments/application_experiment.rb2
-rw-r--r--app/models/concerns/ci/has_status.rb8
-rw-r--r--app/models/issue.rb1
-rw-r--r--app/models/namespaces/traversal/linear.rb48
-rw-r--r--app/models/project.rb2
-rw-r--r--app/models/project_feature_usage.rb25
-rw-r--r--app/models/project_services/asana_service.rb1
-rw-r--r--app/models/project_services/bamboo_service.rb31
-rw-r--r--app/models/project_services/custom_issue_tracker_service.rb6
-rw-r--r--app/models/project_services/datadog_service.rb4
-rw-r--r--app/models/project_services/drone_ci_service.rb2
-rw-r--r--app/models/project_services/external_wiki_service.rb5
-rw-r--r--app/models/project_services/hipchat_service.rb2
-rw-r--r--app/models/project_services/issue_tracker_service.rb6
-rw-r--r--app/models/project_services/jenkins_service.rb23
-rw-r--r--app/models/project_services/jira_service.rb41
-rw-r--r--app/models/project_services/mock_ci_service.rb7
-rw-r--r--app/models/project_services/pushover_service.rb2
-rw-r--r--app/models/project_services/teamcity_service.rb31
-rw-r--r--app/models/project_services/youtrack_service.rb4
-rw-r--r--app/serializers/service_field_entity.rb29
-rw-r--r--app/services/ci/create_web_ide_terminal_service.rb3
-rw-r--r--app/services/upload_service.rb5
-rw-r--r--app/views/layouts/nav/sidebar/_project.html.haml4
-rw-r--r--app/views/projects/issues/index.html.haml2
38 files changed, 320 insertions, 95 deletions
diff --git a/app/assets/javascripts/diffs/components/diff_row.vue b/app/assets/javascripts/diffs/components/diff_row.vue
index ab6890d66b5..fab00a38874 100644
--- a/app/assets/javascripts/diffs/components/diff_row.vue
+++ b/app/assets/javascripts/diffs/components/diff_row.vue
@@ -1,5 +1,5 @@
<script>
-import { GlTooltipDirective, GlIcon, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
+import { GlTooltipDirective, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
import { mapActions, mapGetters, mapState } from 'vuex';
import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
@@ -17,7 +17,6 @@ import * as utils from './diff_row_utils';
export default {
components: {
- GlIcon,
DiffGutterAvatars,
},
directives: {
@@ -203,14 +202,12 @@ export default {
<button
:draggable="glFeatures.dragCommentSelection"
type="button"
- class="add-diff-note note-button js-add-diff-note-button qa-diff-comment"
+ class="add-diff-note unified-diff-components-diff-note-button note-button js-add-diff-note-button qa-diff-comment"
:class="{ 'gl-cursor-grab': dragging }"
:disabled="line.left.commentsDisabled"
@click="handleCommentButton(line.left)"
@dragstart="onDragStart({ ...line.left, index })"
- >
- <gl-icon :size="12" name="comment" />
- </button>
+ ></button>
</span>
</template>
<a
@@ -305,14 +302,12 @@ export default {
<button
:draggable="glFeatures.dragCommentSelection"
type="button"
- class="add-diff-note note-button js-add-diff-note-button qa-diff-comment"
+ class="add-diff-note unified-diff-components-diff-note-button note-button js-add-diff-note-button qa-diff-comment"
:class="{ 'gl-cursor-grab': dragging }"
:disabled="line.right.commentsDisabled"
@click="handleCommentButton(line.right)"
@dragstart="onDragStart({ ...line.right, index })"
- >
- <gl-icon :size="12" name="comment" />
- </button>
+ ></button>
</span>
</template>
<a
diff --git a/app/assets/javascripts/integrations/edit/components/dynamic_field.vue b/app/assets/javascripts/integrations/edit/components/dynamic_field.vue
index a4baca20ac9..3655f94f06f 100644
--- a/app/assets/javascripts/integrations/edit/components/dynamic_field.vue
+++ b/app/assets/javascripts/integrations/edit/components/dynamic_field.vue
@@ -3,7 +3,6 @@
import { GlFormGroup, GlFormCheckbox, GlFormInput, GlFormSelect, GlFormTextarea } from '@gitlab/ui';
import { capitalize, lowerCase, isEmpty } from 'lodash';
import { mapGetters } from 'vuex';
-import { __, sprintf } from '~/locale';
import eventHub from '../event_hub';
export default {
@@ -77,14 +76,6 @@ export default {
isNonEmptyPassword() {
return this.isPassword && !isEmpty(this.value);
},
- label() {
- if (this.isNonEmptyPassword) {
- return sprintf(__('Enter new %{field_title}'), {
- field_title: this.humanizedTitle,
- });
- }
- return this.humanizedTitle;
- },
humanizedTitle() {
return this.title || capitalize(lowerCase(this.name));
},
@@ -136,7 +127,7 @@ export default {
<template>
<gl-form-group
- :label="label"
+ :label="humanizedTitle"
:label-for="fieldId"
:invalid-feedback="__('This field is required.')"
:state="valid"
diff --git a/app/assets/javascripts/integrations/edit/index.js b/app/assets/javascripts/integrations/edit/index.js
index ab9bdd9ca2e..1909f584591 100644
--- a/app/assets/javascripts/integrations/edit/index.js
+++ b/app/assets/javascripts/integrations/edit/index.js
@@ -1,5 +1,6 @@
import Vue from 'vue';
-import { parseBoolean } from '~/lib/utils/common_utils';
+import { convertObjectPropsToCamelCase, parseBoolean } from '~/lib/utils/common_utils';
+
import IntegrationForm from './components/integration_form.vue';
import { createStore } from './store';
@@ -73,7 +74,7 @@ function parseDatasetToProps(data) {
},
learnMorePath,
triggerEvents: JSON.parse(triggerEvents),
- fields: JSON.parse(fields),
+ fields: convertObjectPropsToCamelCase(JSON.parse(fields), { deep: true }),
inheritFromId: parseInt(inheritFromId, 10),
integrationLevel,
id: parseInt(id, 10),
diff --git a/app/assets/javascripts/issuable_bulk_update_actions.js b/app/assets/javascripts/issuable_bulk_update_actions.js
index f507f072253..366a9a8a883 100644
--- a/app/assets/javascripts/issuable_bulk_update_actions.js
+++ b/app/assets/javascripts/issuable_bulk_update_actions.js
@@ -87,7 +87,7 @@ export default {
// From issuable's initial bulk selection
getOriginalCommonIds() {
const labelIds = [];
- this.getElement('.selected-issuable:checked').each((i, el) => {
+ this.getElement('.issuable-list input[type="checkbox"]:checked').each((i, el) => {
labelIds.push(this.getElement(`#${this.prefixId}${el.dataset.id}`).data('labels'));
});
return intersection.apply(this, labelIds);
@@ -100,7 +100,7 @@ export default {
let issuableLabels = [];
// Collect unique label IDs for all checked issues
- this.getElement('.selected-issuable:checked').each((i, el) => {
+ this.getElement('.issuable-list input[type="checkbox"]:checked').each((i, el) => {
issuableLabels = this.getElement(`#${this.prefixId}${el.dataset.id}`).data('labels');
issuableLabels.forEach((labelId) => {
// Store unique IDs
diff --git a/app/assets/javascripts/issuable_bulk_update_sidebar.js b/app/assets/javascripts/issuable_bulk_update_sidebar.js
index ef98db5151a..1f707bc955f 100644
--- a/app/assets/javascripts/issuable_bulk_update_sidebar.js
+++ b/app/assets/javascripts/issuable_bulk_update_sidebar.js
@@ -34,7 +34,7 @@ export default class IssuableBulkUpdateSidebar {
this.$otherFilters = $('.issues-other-filters');
this.$checkAllContainer = $('.check-all-holder');
this.$issueChecks = $('.issue-check');
- this.$issuesList = $('.selected-issuable');
+ this.$issuesList = $('.issuable-list input[type="checkbox"]');
this.$issuableIdsInput = $('#update_issuable_ids');
}
@@ -46,16 +46,14 @@ export default class IssuableBulkUpdateSidebar {
this.$bulkEditSubmitBtn.on('click', () => this.prepForSubmit());
this.$checkAllContainer.on('click', () => this.updateFormState());
- if (this.vueIssuablesListFeature) {
- issueableEventHub.$on('issuables:updateBulkEdit', () => {
- // Danger! Strong coupling ahead!
- // The bulk update sidebar and its dropdowns look for .selected-issuable checkboxes, and get data on which issue
- // is selected by inspecting the DOM. Ideally, we would pass the selected issuable IDs and their properties
- // explicitly, but this component is used in too many places right now to refactor straight away.
+ issueableEventHub.$on('issuables:updateBulkEdit', () => {
+ // Danger! Strong coupling ahead!
+ // The bulk update sidebar and its dropdowns look for checkboxes, and get data on which issue
+ // is selected by inspecting the DOM. Ideally, we would pass the selected issuable IDs and their properties
+ // explicitly, but this component is used in too many places right now to refactor straight away.
- this.updateFormState();
- });
- }
+ this.updateFormState();
+ });
}
initDropdowns() {
@@ -96,7 +94,7 @@ export default class IssuableBulkUpdateSidebar {
}
updateFormState() {
- const noCheckedIssues = !$('.selected-issuable:checked').length;
+ const noCheckedIssues = !$('.issuable-list input[type="checkbox"]:checked').length;
this.toggleSubmitButtonDisabled(noCheckedIssues);
this.updateSelectedIssuableIds();
@@ -166,7 +164,7 @@ export default class IssuableBulkUpdateSidebar {
}
static getCheckedIssueIds() {
- const $checkedIssues = $('.selected-issuable:checked');
+ const $checkedIssues = $('.issuable-list input[type="checkbox"]:checked');
if ($checkedIssues.length > 0) {
return $.map($checkedIssues, (value) => $(value).data('id'));
diff --git a/app/assets/javascripts/issuable_index.js b/app/assets/javascripts/issuable_index.js
index 4856f9781ce..cdeee68b762 100644
--- a/app/assets/javascripts/issuable_index.js
+++ b/app/assets/javascripts/issuable_index.js
@@ -1,7 +1,7 @@
import issuableInitBulkUpdateSidebar from './issuable_init_bulk_update_sidebar';
export default class IssuableIndex {
- constructor(pagePrefix) {
+ constructor(pagePrefix = 'issuable_') {
issuableInitBulkUpdateSidebar.init(pagePrefix);
}
}
diff --git a/app/assets/javascripts/issuable_list/components/issuable_item.vue b/app/assets/javascripts/issuable_list/components/issuable_item.vue
index 92c527c79ff..5d497369f5a 100644
--- a/app/assets/javascripts/issuable_list/components/issuable_item.vue
+++ b/app/assets/javascripts/issuable_list/components/issuable_item.vue
@@ -65,6 +65,9 @@ export default {
labels() {
return this.issuable.labels?.nodes || this.issuable.labels || [];
},
+ labelIdsString() {
+ return JSON.stringify(this.labels.map((label) => label.id));
+ },
assignees() {
return this.issuable.assignees || [];
},
@@ -149,12 +152,13 @@ export default {
</script>
<template>
- <li class="issue gl-px-5!">
+ <li :id="`issuable_${issuable.id}`" class="issue gl-px-5!" :data-labels="labelIdsString">
<div class="issuable-info-container">
<div v-if="showCheckbox" class="issue-check">
<gl-form-checkbox
class="gl-mr-0"
:checked="checked"
+ :data-id="issuable.id"
@input="$emit('checked-input', $event)"
/>
</div>
diff --git a/app/assets/javascripts/issuable_list/components/issuable_list_root.vue b/app/assets/javascripts/issuable_list/components/issuable_list_root.vue
index 1b54ba766ff..6b95c3a578e 100644
--- a/app/assets/javascripts/issuable_list/components/issuable_list_root.vue
+++ b/app/assets/javascripts/issuable_list/components/issuable_list_root.vue
@@ -218,11 +218,13 @@ export default {
},
handleIssuableCheckedInput(issuable, value) {
this.checkedIssuables[this.issuableId(issuable)].checked = value;
+ this.$emit('update-legacy-bulk-edit');
},
handleAllIssuablesCheckedInput(value) {
Object.keys(this.checkedIssuables).forEach((issuableId) => {
this.checkedIssuables[issuableId].checked = value;
});
+ this.$emit('update-legacy-bulk-edit');
},
handleVueDraggableUpdate({ newIndex, oldIndex }) {
this.$emit('reorder', { newIndex, oldIndex });
diff --git a/app/assets/javascripts/issues_list/components/issues_list_app.vue b/app/assets/javascripts/issues_list/components/issues_list_app.vue
index e4bb3ecabd0..d7af388c893 100644
--- a/app/assets/javascripts/issues_list/components/issues_list_app.vue
+++ b/app/assets/javascripts/issues_list/components/issues_list_app.vue
@@ -14,6 +14,7 @@ import {
import axios from '~/lib/utils/axios_utils';
import { convertObjectPropsToCamelCase, getParameterByName } from '~/lib/utils/common_utils';
import { __ } from '~/locale';
+import eventHub from '../eventhub';
import IssueCardTimeInfo from './issue_card_time_info.vue';
export default {
@@ -56,6 +57,7 @@ export default {
filters: sortParams[sortKey] || {},
isLoading: false,
issues: [],
+ showBulkEditSidebar: false,
sortKey: sortKey || CREATED_DESC,
totalIssues: 0,
};
@@ -73,8 +75,15 @@ export default {
},
},
mounted() {
+ eventHub.$on('issuables:toggleBulkEdit', (showBulkEditSidebar) => {
+ this.showBulkEditSidebar = showBulkEditSidebar;
+ });
this.fetchIssues();
},
+ beforeDestroy() {
+ // eslint-disable-next-line @gitlab/no-global-event-off
+ eventHub.$off('issuables:toggleBulkEdit');
+ },
methods: {
fetchIssues(pageToFetch) {
this.isLoading = true;
@@ -101,6 +110,13 @@ export default {
this.isLoading = false;
});
},
+ handleUpdateLegacyBulkEdit() {
+ // If "select all" checkbox was checked, wait for all checkboxes
+ // to be checked before updating IssuableBulkUpdateSidebar class
+ this.$nextTick(() => {
+ eventHub.$emit('issuables:updateBulkEdit');
+ });
+ },
handlePageChange(page) {
this.fetchIssues(page);
},
@@ -159,6 +175,7 @@ export default {
current-tab=""
:issuables-loading="isLoading"
:is-manual-ordering="isManualOrdering"
+ :show-bulk-edit-sidebar="showBulkEditSidebar"
:show-pagination-controls="true"
:total-items="totalIssues"
:current-page="currentPage"
@@ -168,6 +185,7 @@ export default {
@page-change="handlePageChange"
@reorder="handleReorder"
@sort="handleSort"
+ @update-legacy-bulk-edit="handleUpdateLegacyBulkEdit"
>
<template #timeframe="{ issuable = {} }">
<issue-card-time-info :issue="issuable" />
diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js
index ed08a024b59..2503648e6f5 100644
--- a/app/assets/javascripts/labels_select.js
+++ b/app/assets/javascripts/labels_select.js
@@ -526,11 +526,15 @@ export default class LabelsSelect {
}
bindEvents() {
- return $('body').on('change', '.selected-issuable', this.onSelectCheckboxIssue);
+ return $('body').on(
+ 'change',
+ '.issuable-list input[type="checkbox"]',
+ this.onSelectCheckboxIssue,
+ );
}
// eslint-disable-next-line class-methods-use-this
onSelectCheckboxIssue() {
- if ($('.selected-issuable:checked').length) {
+ if ($('.issuable-list input[type="checkbox"]:checked').length) {
return;
}
return $('.issues-bulk-update .labels-filter .dropdown-toggle-text').text(__('Label'));
diff --git a/app/assets/javascripts/pages/projects/issues/index/index.js b/app/assets/javascripts/pages/projects/issues/index/index.js
index 366f8dc61bc..85489ae8687 100644
--- a/app/assets/javascripts/pages/projects/issues/index/index.js
+++ b/app/assets/javascripts/pages/projects/issues/index/index.js
@@ -20,7 +20,12 @@ initFilteredSearch({
useDefaultState: true,
});
-new IssuableIndex(ISSUABLE_INDEX.ISSUE);
+if (gon.features?.vueIssuesList) {
+ new IssuableIndex();
+} else {
+ new IssuableIndex(ISSUABLE_INDEX.ISSUE);
+}
+
new ShortcutsNavigation();
new UsersSelect();
diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss
index 190bdcb1efd..801dd44be8e 100644
--- a/app/assets/stylesheets/pages/notes.scss
+++ b/app/assets/stylesheets/pages/notes.scss
@@ -771,6 +771,26 @@ $system-note-svg-size: 16px;
}
}
+.unified-diff-components-diff-note-button {
+ &::before {
+ background-color: $blue-500;
+ mask-image: asset_url('icons-stacked.svg#comment');
+ mask-repeat: no-repeat;
+ mask-size: cover;
+ mask-position: center;
+ content: '';
+ width: 12px;
+ height: 12px;
+ }
+
+ &:hover,
+ &.inverted {
+ &::before {
+ background-color: $white;
+ }
+ }
+}
+
.disabled-comment {
background-color: $gray-light;
border-radius: $border-radius-base;
diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb
index ff4ccc4f7af..c1ed017d4a6 100644
--- a/app/controllers/projects/issues_controller.rb
+++ b/app/controllers/projects/issues_controller.rb
@@ -45,6 +45,7 @@ class Projects::IssuesController < Projects::ApplicationController
push_frontend_feature_flag(:vue_issuables_list, project)
push_frontend_feature_flag(:usage_data_design_action, project, default_enabled: true)
push_frontend_feature_flag(:improved_emoji_picker, project, default_enabled: :yaml)
+ push_frontend_feature_flag(:vue_issues_list, project)
end
before_action only: :show do
diff --git a/app/experiments/application_experiment.rb b/app/experiments/application_experiment.rb
index ed0d146af8c..01105f6cec4 100644
--- a/app/experiments/application_experiment.rb
+++ b/app/experiments/application_experiment.rb
@@ -27,7 +27,7 @@ class ApplicationExperiment < Gitlab::Experiment # rubocop:disable Gitlab/Namesp
# track the event, and mix in the experiment signature data
Gitlab::Tracking.event(name, action.to_s, **event_args.merge(
context: (event_args[:context] || []) << SnowplowTracker::SelfDescribingJson.new(
- 'iglu:com.gitlab/gitlab_experiment/jsonschema/0-3-0', signature
+ 'iglu:com.gitlab/gitlab_experiment/jsonschema/1-0-0', signature
)
))
end
diff --git a/app/models/concerns/ci/has_status.rb b/app/models/concerns/ci/has_status.rb
index 0412f7a072b..aec701a4b86 100644
--- a/app/models/concerns/ci/has_status.rb
+++ b/app/models/concerns/ci/has_status.rb
@@ -43,6 +43,14 @@ module Ci
def completed_statuses
COMPLETED_STATUSES.map(&:to_sym)
end
+
+ def blocked_statuses
+ BLOCKED_STATUS.map(&:to_sym)
+ end
+
+ def completed_and_blocked_statuses
+ completed_statuses + blocked_statuses
+ end
end
included do
diff --git a/app/models/issue.rb b/app/models/issue.rb
index 16b30f193c7..31c16f9546c 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -115,7 +115,6 @@ class Issue < ApplicationRecord
scope :preload_associated_models, -> { preload(:assignees, :labels, project: :namespace) }
scope :with_web_entity_associations, -> { preload(:author, :project) }
- scope :with_api_entity_associations, -> { preload(:timelogs, :assignees, :author, :notes, :labels, project: [:route, { namespace: :route }] ) }
scope :with_label_attributes, ->(label_attributes) { joins(:labels).where(labels: label_attributes) }
scope :with_alert_management_alerts, -> { joins(:alert_management_alert) }
scope :with_prometheus_alert_events, -> { joins(:issues_prometheus_alert_events) }
diff --git a/app/models/namespaces/traversal/linear.rb b/app/models/namespaces/traversal/linear.rb
index 8c042e5409d..294ef83b9b4 100644
--- a/app/models/namespaces/traversal/linear.rb
+++ b/app/models/namespaces/traversal/linear.rb
@@ -38,15 +38,31 @@ module Namespaces
module Linear
extend ActiveSupport::Concern
+ UnboundedSearch = Class.new(StandardError)
+
included do
after_create :sync_traversal_ids, if: -> { sync_traversal_ids? }
after_update :sync_traversal_ids, if: -> { sync_traversal_ids? && saved_change_to_parent_id? }
+
+ scope :traversal_ids_contains, ->(ids) { where("traversal_ids @> (?)", ids) }
end
def sync_traversal_ids?
Feature.enabled?(:sync_traversal_ids, root_ancestor, default_enabled: :yaml)
end
+ def use_traversal_ids?
+ Feature.enabled?(:use_traversal_ids, root_ancestor, default_enabled: :yaml)
+ end
+
+ def self_and_descendants
+ if use_traversal_ids?
+ lineage(self)
+ else
+ super
+ end
+ end
+
private
# Update the traversal_ids for the full hierarchy.
@@ -58,6 +74,38 @@ module Namespaces
Namespace::TraversalHierarchy.for_namespace(root_ancestor).sync_traversal_ids!
end
+
+ # Make sure we drop the STI `type = 'Group'` condition for better performance.
+ # Logically equivalent so long as hierarchies remain homogeneous.
+ def without_sti_condition
+ self.class.unscope(where: :type)
+ end
+
+ # Search this namespace's lineage. Bound inclusively by top node.
+ def lineage(top)
+ raise UnboundedSearch.new('Must bound search by a top') unless top
+
+ without_sti_condition
+ .traversal_ids_contains(latest_traversal_ids(top))
+ end
+
+ # traversal_ids are a cached value.
+ #
+ # The traversal_ids value in a loaded object can become stale when compared
+ # to the database value. For example, if you load a hierarchy and then move
+ # a group, any previously loaded descendant objects will have out of date
+ # traversal_ids.
+ #
+ # To solve this problem, we never depend on the object's traversal_ids
+ # value. We always query the database first with a sub-select for the
+ # latest traversal_ids.
+ #
+ # Note that ActiveRecord will cache query results. You can avoid this by
+ # using `Model.uncached { ... }`
+ def latest_traversal_ids(namespace = self)
+ without_sti_condition.where('id = (?)', namespace)
+ .select('traversal_ids as latest_traversal_ids')
+ end
end
end
end
diff --git a/app/models/project.rb b/app/models/project.rb
index d363c6c221e..61f165b9daa 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -623,7 +623,7 @@ class Project < ApplicationRecord
end
def self.with_web_entity_associations
- preload(:project_feature, :route, :creator, :group, namespace: [:route, :owner])
+ preload(:project_feature, :route, :creator, group: :parent, namespace: [:route, :owner])
end
def self.eager_load_namespace_and_owner
diff --git a/app/models/project_feature_usage.rb b/app/models/project_feature_usage.rb
index 4f445758653..02051310af7 100644
--- a/app/models/project_feature_usage.rb
+++ b/app/models/project_feature_usage.rb
@@ -20,12 +20,29 @@ class ProjectFeatureUsage < ApplicationRecord
end
def log_jira_dvcs_integration_usage(cloud: true)
- transaction(requires_new: true) do
- save unless persisted?
- touch(self.class.jira_dvcs_integration_field(cloud: cloud))
- end
+ integration_field = self.class.jira_dvcs_integration_field(cloud: cloud)
+
+ # The feature usage is used only once later to query the feature usage in a
+ # long date range. Therefore, we just need to update the timestamp once per
+ # day
+ return if persisted? && updated_today?(integration_field)
+
+ persist_jira_dvcs_usage(integration_field)
+ end
+
+ private
+
+ def updated_today?(integration_field)
+ self[integration_field].present? && self[integration_field].today?
+ end
+
+ def persist_jira_dvcs_usage(integration_field)
+ assign_attributes(integration_field => Time.current)
+ save
rescue ActiveRecord::RecordNotUnique
reset
retry
end
end
+
+ProjectFeatureUsage.prepend_if_ee('EE::ProjectFeatureUsage')
diff --git a/app/models/project_services/asana_service.rb b/app/models/project_services/asana_service.rb
index c4fcdcc05c5..4fbbca9d5e9 100644
--- a/app/models/project_services/asana_service.rb
+++ b/app/models/project_services/asana_service.rb
@@ -36,6 +36,7 @@ https://app.asana.com/0/developer-console'
{
type: 'text',
name: 'api_key',
+ title: _('API key'),
placeholder: s_('AsanaService|User Personal Access Token. User must have access to task, all comments will be attributed to this user.'),
required: true
},
diff --git a/app/models/project_services/bamboo_service.rb b/app/models/project_services/bamboo_service.rb
index 8c1f4fef09b..8c10b54c483 100644
--- a/app/models/project_services/bamboo_service.rb
+++ b/app/models/project_services/bamboo_service.rb
@@ -48,13 +48,30 @@ class BambooService < CiService
def fields
[
- { type: 'text', name: 'bamboo_url',
- placeholder: s_('BambooService|Bamboo root URL like https://bamboo.example.com'), required: true },
- { type: 'text', name: 'build_key',
- placeholder: s_('BambooService|Bamboo build plan key like KEY'), required: true },
- { type: 'text', name: 'username',
- placeholder: s_('BambooService|A user with API access, if applicable') },
- { type: 'password', name: 'password' }
+ {
+ type: 'text',
+ name: 'bamboo_url',
+ title: s_('BambooService|Bamboo URL'),
+ placeholder: s_('BambooService|Bamboo root URL like https://bamboo.example.com'),
+ required: true
+ },
+ {
+ type: 'text',
+ name: 'build_key',
+ placeholder: s_('BambooService|Bamboo build plan key like KEY'),
+ required: true
+ },
+ {
+ type: 'text',
+ name: 'username',
+ placeholder: s_('BambooService|A user with API access, if applicable')
+ },
+ {
+ type: 'password',
+ name: 'password',
+ non_empty_password_title: s_('ProjectService|Enter new password'),
+ non_empty_password_help: s_('ProjectService|Leave blank to use your current password')
+ }
]
end
diff --git a/app/models/project_services/custom_issue_tracker_service.rb b/app/models/project_services/custom_issue_tracker_service.rb
index fc58ba27c3d..aab8661ec55 100644
--- a/app/models/project_services/custom_issue_tracker_service.rb
+++ b/app/models/project_services/custom_issue_tracker_service.rb
@@ -17,9 +17,9 @@ class CustomIssueTrackerService < IssueTrackerService
def fields
[
- { type: 'text', name: 'project_url', placeholder: 'Project url', required: true },
- { type: 'text', name: 'issues_url', placeholder: 'Issue url', required: true },
- { type: 'text', name: 'new_issue_url', placeholder: 'New Issue url', required: true }
+ { type: 'text', name: 'project_url', title: _('Project URL'), required: true },
+ { type: 'text', name: 'issues_url', title: s_('ProjectService|Issue URL'), required: true },
+ { type: 'text', name: 'new_issue_url', title: s_('ProjectService|New issue URL'), required: true }
]
end
end
diff --git a/app/models/project_services/datadog_service.rb b/app/models/project_services/datadog_service.rb
index a48dea71645..9a2d99c46c9 100644
--- a/app/models/project_services/datadog_service.rb
+++ b/app/models/project_services/datadog_service.rb
@@ -78,7 +78,9 @@ class DatadogService < Service
{
type: 'password',
name: 'api_key',
- title: 'API key',
+ title: _('API key'),
+ non_empty_password_title: s_('ProjectService|Enter new API key'),
+ non_empty_password_help: s_('ProjectService|Leave blank to use your current API key'),
help: "<a href=\"#{api_keys_url}\" target=\"_blank\">API key</a> used for authentication with Datadog",
required: true
},
diff --git a/app/models/project_services/drone_ci_service.rb b/app/models/project_services/drone_ci_service.rb
index 5a49f780d46..7f46d2550e3 100644
--- a/app/models/project_services/drone_ci_service.rb
+++ b/app/models/project_services/drone_ci_service.rb
@@ -93,7 +93,7 @@ class DroneCiService < CiService
def fields
[
{ type: 'text', name: 'token', placeholder: 'Drone CI project specific token', required: true },
- { type: 'text', name: 'drone_url', placeholder: 'http://drone.example.com', required: true },
+ { type: 'text', name: 'drone_url', title: s_('ProjectService|Drone URL'), placeholder: 'http://drone.example.com', required: true },
{ type: 'checkbox', name: 'enable_ssl_verification', title: "Enable SSL verification" }
]
end
diff --git a/app/models/project_services/external_wiki_service.rb b/app/models/project_services/external_wiki_service.rb
index 0a09000fff4..7dd5b1fb00f 100644
--- a/app/models/project_services/external_wiki_service.rb
+++ b/app/models/project_services/external_wiki_service.rb
@@ -6,7 +6,7 @@ class ExternalWikiService < Service
validates :external_wiki_url, presence: true, public_url: true, if: :activated?
def title
- s_('ExternalWikiService|External Wiki')
+ s_('ExternalWikiService|External wiki')
end
def description
@@ -22,7 +22,8 @@ class ExternalWikiService < Service
{
type: 'text',
name: 'external_wiki_url',
- placeholder: s_('ExternalWikiService|The URL of the external Wiki'),
+ title: s_('ExternalWikiService|External wiki URL'),
+ placeholder: s_('ExternalWikiService|The URL of the external wiki'),
required: true
}
]
diff --git a/app/models/project_services/hipchat_service.rb b/app/models/project_services/hipchat_service.rb
index 22c2aebaec3..cd49c6d253d 100644
--- a/app/models/project_services/hipchat_service.rb
+++ b/app/models/project_services/hipchat_service.rb
@@ -39,7 +39,7 @@ class HipchatService < Service
{ type: 'text', name: 'room', placeholder: 'Room name or ID' },
{ type: 'checkbox', name: 'notify' },
{ type: 'select', name: 'color', choices: %w(yellow red green purple gray random) },
- { type: 'text', name: 'api_version',
+ { type: 'text', name: 'api_version', title: _('API version'),
placeholder: 'Leave blank for default (v2)' },
{ type: 'text', name: 'server',
placeholder: 'Leave blank for default. https://hipchat.example.com' },
diff --git a/app/models/project_services/issue_tracker_service.rb b/app/models/project_services/issue_tracker_service.rb
index 694374e9548..19a5b4a74bb 100644
--- a/app/models/project_services/issue_tracker_service.rb
+++ b/app/models/project_services/issue_tracker_service.rb
@@ -73,9 +73,9 @@ class IssueTrackerService < Service
def fields
[
- { type: 'text', name: 'project_url', placeholder: 'Project url', required: true },
- { type: 'text', name: 'issues_url', placeholder: 'Issue url', required: true },
- { type: 'text', name: 'new_issue_url', placeholder: 'New Issue url', required: true }
+ { type: 'text', name: 'project_url', title: _('Project URL'), required: true },
+ { type: 'text', name: 'issues_url', title: s_('ProjectService|Issue URL'), required: true },
+ { type: 'text', name: 'new_issue_url', title: s_('ProjectService|New issue URL'), required: true }
]
end
diff --git a/app/models/project_services/jenkins_service.rb b/app/models/project_services/jenkins_service.rb
index 63ecfc66877..3a68e7efb47 100644
--- a/app/models/project_services/jenkins_service.rb
+++ b/app/models/project_services/jenkins_service.rb
@@ -77,15 +77,30 @@ class JenkinsService < CiService
def fields
[
{
- type: 'text', name: 'jenkins_url',
+ type: 'text',
+ name: 'jenkins_url',
+ title: s_('ProjectService|Jenkins URL'),
+ required: true,
placeholder: 'Jenkins URL like http://jenkins.example.com'
},
{
- type: 'text', name: 'project_name', placeholder: 'Project Name',
+ type: 'text',
+ name: 'project_name',
+ required: true,
+ placeholder: 'Project Name',
help: 'The URL-friendly project name. Example: my_project_name'
},
- { type: 'text', name: 'username' },
- { type: 'password', name: 'password' }
+ {
+ type: 'text',
+ name: 'username',
+ required: true
+ },
+ {
+ type: 'password',
+ name: 'password',
+ non_empty_password_title: s_('ProjectService|Enter new password'),
+ non_empty_password_help: s_('ProjectService|Leave blank to use your current password')
+ }
]
end
end
diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb
index 5857d86f921..415e347be6c 100644
--- a/app/models/project_services/jira_service.rb
+++ b/app/models/project_services/jira_service.rb
@@ -128,11 +128,42 @@ class JiraService < IssueTrackerService
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: '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'),
+ non_empty_password_title: s_('JiraService|Enter new password or API token'),
+ non_empty_password_help: s_('JiraService|Leave blank to use your current 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 })
+ }
]
end
diff --git a/app/models/project_services/mock_ci_service.rb b/app/models/project_services/mock_ci_service.rb
index c5e5f4f6400..bd6344c6e1a 100644
--- a/app/models/project_services/mock_ci_service.rb
+++ b/app/models/project_services/mock_ci_service.rb
@@ -21,10 +21,13 @@ class MockCiService < CiService
def fields
[
- { type: 'text',
+ {
+ type: 'text',
name: 'mock_service_url',
+ title: s_('ProjectService|Mock service URL'),
placeholder: 'http://localhost:4004',
- required: true }
+ required: true
+ }
]
end
diff --git a/app/models/project_services/pushover_service.rb b/app/models/project_services/pushover_service.rb
index 7324890551c..1781ec7456d 100644
--- a/app/models/project_services/pushover_service.rb
+++ b/app/models/project_services/pushover_service.rb
@@ -20,7 +20,7 @@ class PushoverService < Service
def fields
[
- { type: 'text', name: 'api_key', placeholder: s_('PushoverService|Your application key'), required: true },
+ { type: 'text', name: 'api_key', title: _('API key'), placeholder: s_('PushoverService|Your application key'), required: true },
{ type: 'text', name: 'user_key', placeholder: s_('PushoverService|Your user key'), required: true },
{ type: 'text', name: 'device', placeholder: s_('PushoverService|Leave blank for all active devices') },
{ type: 'select', name: 'priority', required: true, choices:
diff --git a/app/models/project_services/teamcity_service.rb b/app/models/project_services/teamcity_service.rb
index 209b691ef98..60d107d7557 100644
--- a/app/models/project_services/teamcity_service.rb
+++ b/app/models/project_services/teamcity_service.rb
@@ -65,13 +65,30 @@ class TeamcityService < CiService
def fields
[
- { type: 'text', name: 'teamcity_url',
- placeholder: 'TeamCity root URL like https://teamcity.example.com', required: true },
- { type: 'text', name: 'build_type',
- placeholder: 'Build configuration ID', required: true },
- { type: 'text', name: 'username',
- placeholder: 'A user with permissions to trigger a manual build' },
- { type: 'password', name: 'password' }
+ {
+ type: 'text',
+ name: 'teamcity_url',
+ title: s_('ProjectService|TeamCity URL'),
+ placeholder: 'TeamCity root URL like https://teamcity.example.com',
+ required: true
+ },
+ {
+ type: 'text',
+ name: 'build_type',
+ placeholder: 'Build configuration ID',
+ required: true
+ },
+ {
+ type: 'text',
+ name: 'username',
+ placeholder: 'A user with permissions to trigger a manual build'
+ },
+ {
+ type: 'password',
+ name: 'password',
+ non_empty_password_title: s_('ProjectService|Enter new password'),
+ non_empty_password_help: s_('ProjectService|Leave blank to use your current password')
+ }
]
end
diff --git a/app/models/project_services/youtrack_service.rb b/app/models/project_services/youtrack_service.rb
index 7fb3bde44a5..30abd0159b3 100644
--- a/app/models/project_services/youtrack_service.rb
+++ b/app/models/project_services/youtrack_service.rb
@@ -26,8 +26,8 @@ class YoutrackService < IssueTrackerService
def fields
[
- { type: 'text', name: 'project_url', title: 'Project URL', placeholder: 'Project URL', required: true },
- { type: 'text', name: 'issues_url', title: 'Issue URL', placeholder: 'Issue URL', required: true }
+ { type: 'text', name: 'project_url', title: _('Project URL'), required: true },
+ { type: 'text', name: 'issues_url', title: s_('ProjectService|Issue URL'), required: true }
]
end
end
diff --git a/app/serializers/service_field_entity.rb b/app/serializers/service_field_entity.rb
index 08e08ae187f..960e216906e 100644
--- a/app/serializers/service_field_entity.rb
+++ b/app/serializers/service_field_entity.rb
@@ -2,14 +2,22 @@
class ServiceFieldEntity < Grape::Entity
include RequestAwareEntity
+ include Gitlab::Utils::StrongMemoize
- expose :type, :name, :title, :placeholder, :required, :choices, :help
+ expose :type, :name, :placeholder, :required, :choices
+
+ expose :title do |field|
+ non_empty_password?(field) ? field[:non_empty_password_title] : field[:title]
+ end
+
+ expose :help do |field|
+ non_empty_password?(field) ? field[:non_empty_password_help] : field[:help]
+ end
expose :value do |field|
- # field[:name] is not user input and so can assume is safe
- value = service.public_send(field[:name]) # rubocop:disable GitlabSecurity/PublicSend
+ value = value_for(field)
- if field[:type] == 'password' && value.present?
+ if non_empty_password?(field)
'true'
elsif field[:type] == 'checkbox'
ActiveRecord::Type::Boolean.new.deserialize(value).to_s
@@ -23,4 +31,17 @@ class ServiceFieldEntity < Grape::Entity
def service
request.service
end
+
+ def value_for(field)
+ strong_memoize(:value_for) do
+ # field[:name] is not user input and so can assume is safe
+ service.public_send(field[:name]) # rubocop:disable GitlabSecurity/PublicSend
+ end
+ end
+
+ def non_empty_password?(field)
+ strong_memoize(:non_empty_password) do
+ field[:type] == 'password' && value_for(field).present?
+ end
+ end
end
diff --git a/app/services/ci/create_web_ide_terminal_service.rb b/app/services/ci/create_web_ide_terminal_service.rb
index 785d82094b9..3b89a599180 100644
--- a/app/services/ci/create_web_ide_terminal_service.rb
+++ b/app/services/ci/create_web_ide_terminal_service.rb
@@ -58,7 +58,8 @@ module Ci
builds: [terminal_build_seed]
}
- Gitlab::Ci::Pipeline::Seed::Stage.new(pipeline, attributes, [])
+ seed_context = Gitlab::Ci::Pipeline::Seed::Context.new(pipeline)
+ Gitlab::Ci::Pipeline::Seed::Stage.new(seed_context, attributes, [])
end
def terminal_build_seed
diff --git a/app/services/upload_service.rb b/app/services/upload_service.rb
index ba6ead41836..2bf4fcd90a0 100644
--- a/app/services/upload_service.rb
+++ b/app/services/upload_service.rb
@@ -1,6 +1,9 @@
# frozen_string_literal: true
class UploadService
+ # Temporarily introduced for upload API: https://gitlab.com/gitlab-org/gitlab/-/issues/325788
+ attr_accessor :override_max_attachment_size
+
def initialize(model, file, uploader_class = FileUploader, **uploader_context)
@model, @file, @uploader_class, @uploader_context = model, file, uploader_class, uploader_context
end
@@ -19,6 +22,6 @@ class UploadService
attr_reader :model, :file, :uploader_class, :uploader_context
def max_attachment_size
- Gitlab::CurrentSettings.max_attachment_size.megabytes.to_i
+ override_max_attachment_size || Gitlab::CurrentSettings.max_attachment_size.megabytes.to_i
end
end
diff --git a/app/views/layouts/nav/sidebar/_project.html.haml b/app/views/layouts/nav/sidebar/_project.html.haml
index 4c331dbd69d..0e06accf2d1 100644
--- a/app/views/layouts/nav/sidebar/_project.html.haml
+++ b/app/views/layouts/nav/sidebar/_project.html.haml
@@ -346,12 +346,12 @@
.nav-icon-container
= sprite_icon('external-link')
%span.nav-item-name
- = _('External Wiki')
+ = s_('ExternalWikiService|External wiki')
%ul.sidebar-sub-level-items.is-fly-out-only
= nav_link(html_options: { class: "fly-out-top-item" } ) do
= link_to external_wiki_url do
%strong.fly-out-top-item-name
- = _('External Wiki')
+ = s_('ExternalWikiService|External wiki')
- if project_nav_tab? :snippets
= nav_link(controller: :snippets) do
diff --git a/app/views/projects/issues/index.html.haml b/app/views/projects/issues/index.html.haml
index 9c54b117f4c..3b35bd7544a 100644
--- a/app/views/projects/issues/index.html.haml
+++ b/app/views/projects/issues/index.html.haml
@@ -26,6 +26,8 @@
has_issuable_health_status_feature: Gitlab.ee? && @project.feature_available?(:issuable_health_status).to_s,
has_issue_weights_feature: Gitlab.ee? && @project.feature_available?(:issue_weights).to_s,
issues_path: project_issues_path(@project) } }
+ - if @can_bulk_update
+ = render 'shared/issuable/bulk_update_sidebar', type: :issues
- else
= render 'shared/issuable/search_bar', type: :issues