diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-03-25 12:09:19 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-03-25 12:09:19 +0000 |
commit | a156fc95eb8499fec9cd081d30629f0faf18bfe9 (patch) | |
tree | ff59d44794ba9d8084e4d59057ec9507b3ba8e2f /app | |
parent | 618be8f52d6349533c709a1d702e45b84338c36a (diff) | |
download | gitlab-ce-a156fc95eb8499fec9cd081d30629f0faf18bfe9.tar.gz |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
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 |