Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2022-06-22 15:09:48 +00:00
parent 421f6c92d5
commit 640007842a
39 changed files with 433 additions and 157 deletions

View File

@ -5,7 +5,7 @@ review-cleanup:
extends: extends:
- .default-retry - .default-retry
- .review:rules:review-cleanup - .review:rules:review-cleanup
image: ${REGISTRY_HOST}/${REGISTRY_GROUP}/gitlab-build-images:gitlab-helm3.5-kubectl1.17 image: ${REGISTRY_HOST}/${REGISTRY_GROUP}/gitlab-build-images:gitlab-gcloud-helm3.5-kubectl1.17
stage: prepare stage: prepare
environment: environment:
name: review/${CI_COMMIT_REF_SLUG}${FREQUENCY} name: review/${CI_COMMIT_REF_SLUG}${FREQUENCY}

View File

@ -2,6 +2,7 @@
import { GlDropdown, GlDropdownItem, GlIcon } from '@gitlab/ui'; import { GlDropdown, GlDropdownItem, GlIcon } from '@gitlab/ui';
import { mapActions, mapGetters, mapState } from 'vuex'; import { mapActions, mapGetters, mapState } from 'vuex';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { setUrlParams, visitUrl } from '~/lib/utils/url_utility';
import PreviewItem from './preview_item.vue'; import PreviewItem from './preview_item.vue';
import DraftsCount from './drafts_count.vue'; import DraftsCount from './drafts_count.vue';
@ -17,6 +18,7 @@ export default {
computed: { computed: {
...mapState('diffs', ['viewDiffsFileByFile']), ...mapState('diffs', ['viewDiffsFileByFile']),
...mapGetters('batchComments', ['draftsCount', 'sortedDrafts']), ...mapGetters('batchComments', ['draftsCount', 'sortedDrafts']),
...mapGetters(['getNoteableData']),
}, },
methods: { methods: {
...mapActions('diffs', ['setCurrentFileHash']), ...mapActions('diffs', ['setCurrentFileHash']),
@ -24,12 +26,21 @@ export default {
isLast(index) { isLast(index) {
return index === this.sortedDrafts.length - 1; return index === this.sortedDrafts.length - 1;
}, },
isOnLatestDiff(draft) {
return draft.position?.head_sha === this.getNoteableData.diff_head_sha;
},
async onClickDraft(draft) { async onClickDraft(draft) {
if (this.viewDiffsFileByFile && draft.file_hash) { if (this.viewDiffsFileByFile && draft.file_hash) {
await this.setCurrentFileHash(draft.file_hash); await this.setCurrentFileHash(draft.file_hash);
} }
await this.scrollToDraft(draft); if (draft.position && !this.isOnLatestDiff(draft)) {
const url = new URL(setUrlParams({ commit_id: draft.position.head_sha }));
url.hash = `note_${draft.id}`;
visitUrl(url.toString());
} else {
await this.scrollToDraft(draft);
}
}, },
}, },
}; };
@ -52,6 +63,7 @@ export default {
data-testid="preview-item" data-testid="preview-item"
@click="onClickDraft(draft)" @click="onClickDraft(draft)"
> >
{{ isOnLatestDiff(draft) }}
<preview-item :draft="draft" :is-last="isLast(index)" /> <preview-item :draft="draft" :is-last="isLast(index)" />
</gl-dropdown-item> </gl-dropdown-item>
</gl-dropdown> </gl-dropdown>

View File

@ -11,6 +11,8 @@ import { convertToCamelCase, convertToSnakeCase } from './text_utility';
import { isObject } from './type_utility'; import { isObject } from './type_utility';
import { getLocationHash } from './url_utility'; import { getLocationHash } from './url_utility';
export const NO_SCROLL_TO_HASH_CLASS = 'js-no-scroll-to-hash';
export const getPagePath = (index = 0) => { export const getPagePath = (index = 0) => {
const { page = '' } = document.body.dataset; const { page = '' } = document.body.dataset;
return page.split(':')[index]; return page.split(':')[index];
@ -68,6 +70,10 @@ export const handleLocationHash = () => {
hash = decodeURIComponent(hash); hash = decodeURIComponent(hash);
const target = document.getElementById(hash) || document.getElementById(`user-content-${hash}`); const target = document.getElementById(hash) || document.getElementById(`user-content-${hash}`);
// Allow targets to opt out of scroll behavior
if (target?.classList.contains(NO_SCROLL_TO_HASH_CLASS)) return;
const fixedTabs = document.querySelector('.js-tabs-affix'); const fixedTabs = document.querySelector('.js-tabs-affix');
const fixedDiffStats = document.querySelector('.js-diff-files-changed'); const fixedDiffStats = document.querySelector('.js-diff-files-changed');
const fixedNav = document.querySelector('.navbar-gitlab'); const fixedNav = document.querySelector('.navbar-gitlab');

View File

@ -1,33 +1,27 @@
import createFlash from '~/flash'; import createFlash from '~/flash';
import { sanitize } from '~/lib/dompurify'; import { sanitize } from '~/lib/dompurify';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import { historyPushState } from '~/lib/utils/common_utils';
import { __ } from '~/locale'; import { __ } from '~/locale';
import { GlTabsBehavior, TAB_SHOWN_EVENT } from '~/tabs'; import { GlTabsBehavior, TAB_SHOWN_EVENT, HISTORY_TYPE_HASH } from '~/tabs';
export default class Milestone { export default class Milestone {
constructor() { constructor() {
this.tabsEl = document.querySelector('.js-milestone-tabs'); this.tabsEl = document.querySelector('.js-milestone-tabs');
this.glTabs = new GlTabsBehavior(this.tabsEl);
this.loadedTabs = new WeakSet(); this.loadedTabs = new WeakSet();
this.bindTabsSwitching(); this.bindTabsSwitching();
this.loadInitialTab(); // eslint-disable-next-line no-new
new GlTabsBehavior(this.tabsEl, { history: HISTORY_TYPE_HASH });
} }
bindTabsSwitching() { bindTabsSwitching() {
this.tabsEl.addEventListener(TAB_SHOWN_EVENT, (event) => { this.tabsEl.addEventListener(TAB_SHOWN_EVENT, (event) => {
const tab = event.target; const tab = event.target;
const { activeTabPanel } = event.detail; const { activeTabPanel } = event.detail;
historyPushState(tab.getAttribute('href'));
this.loadTab(tab, activeTabPanel); this.loadTab(tab, activeTabPanel);
}); });
} }
loadInitialTab() {
const tab = this.tabsEl.querySelector(`a[href="${window.location.hash}"]`);
this.glTabs.activateTab(tab || this.glTabs.activeTab);
}
loadTab(tab, tabPanel) { loadTab(tab, tabPanel) {
const { endpoint } = tab.dataset; const { endpoint } = tab.dataset;

View File

@ -14,3 +14,6 @@ export const ATTR_ROLE = 'role';
export const ATTR_TABINDEX = 'tabindex'; export const ATTR_TABINDEX = 'tabindex';
export const TAB_SHOWN_EVENT = 'gl-tab-shown'; export const TAB_SHOWN_EVENT = 'gl-tab-shown';
export const HISTORY_TYPE_HASH = 'hash';
export const ALLOWED_HISTORY_TYPES = [HISTORY_TYPE_HASH];

View File

@ -1,4 +1,5 @@
import { uniqueId } from 'lodash'; import { uniqueId } from 'lodash';
import { historyReplaceState, NO_SCROLL_TO_HASH_CLASS } from '~/lib/utils/common_utils';
import { import {
ACTIVE_TAB_CLASSES, ACTIVE_TAB_CLASSES,
ATTR_ROLE, ATTR_ROLE,
@ -12,9 +13,11 @@ import {
KEY_CODE_RIGHT, KEY_CODE_RIGHT,
KEY_CODE_DOWN, KEY_CODE_DOWN,
TAB_SHOWN_EVENT, TAB_SHOWN_EVENT,
HISTORY_TYPE_HASH,
ALLOWED_HISTORY_TYPES,
} from './constants'; } from './constants';
export { TAB_SHOWN_EVENT }; export { TAB_SHOWN_EVENT, HISTORY_TYPE_HASH };
/** /**
* The `GlTabsBehavior` class adds interactivity to tabs created by the `gl_tabs_nav` and * The `GlTabsBehavior` class adds interactivity to tabs created by the `gl_tabs_nav` and
@ -88,9 +91,13 @@ export class GlTabsBehavior {
/** /**
* Create a GlTabsBehavior instance. * Create a GlTabsBehavior instance.
* *
* @param {HTMLElement} el The element created by the Rails `gl_tabs_nav` helper. * @param {HTMLElement} el - The element created by the Rails `gl_tabs_nav` helper.
* @param {Object} [options]
* @param {'hash' | null} [options.history=null] - Sets the type of routing GlTabs will use when navigating between tabs.
* 'hash': Updates the URL hash with the current tab ID.
* null: No routing mechanism will be used.
*/ */
constructor(el) { constructor(el, { history = null } = {}) {
if (!el) { if (!el) {
throw new Error('Cannot instantiate GlTabsBehavior without an element'); throw new Error('Cannot instantiate GlTabsBehavior without an element');
} }
@ -100,8 +107,11 @@ export class GlTabsBehavior {
this.tabs = this.getTabs(); this.tabs = this.getTabs();
this.activeTab = null; this.activeTab = null;
this.history = ALLOWED_HISTORY_TYPES.includes(history) ? history : null;
this.setAccessibilityAttrs(); this.setAccessibilityAttrs();
this.bindEvents(); this.bindEvents();
if (this.history === HISTORY_TYPE_HASH) this.loadInitialTab();
} }
setAccessibilityAttrs() { setAccessibilityAttrs() {
@ -128,6 +138,7 @@ export class GlTabsBehavior {
tab.setAttribute(ATTR_ARIA_CONTROLS, tabPanel.id); tab.setAttribute(ATTR_ARIA_CONTROLS, tabPanel.id);
} }
tabPanel.classList.add(NO_SCROLL_TO_HASH_CLASS);
tabPanel.setAttribute(ATTR_ROLE, 'tabpanel'); tabPanel.setAttribute(ATTR_ROLE, 'tabpanel');
tabPanel.setAttribute(ATTR_ARIA_LABELLEDBY, tab.id); tabPanel.setAttribute(ATTR_ARIA_LABELLEDBY, tab.id);
}); });
@ -164,6 +175,11 @@ export class GlTabsBehavior {
}); });
} }
loadInitialTab() {
const tab = this.tabList.querySelector(`a[href="${CSS.escape(window.location.hash)}"]`);
this.activateTab(tab || this.activeTab);
}
activatePreviousTab() { activatePreviousTab() {
const currentTabIndex = this.tabs.indexOf(this.activeTab); const currentTabIndex = this.tabs.indexOf(this.activeTab);
@ -216,6 +232,7 @@ export class GlTabsBehavior {
const tabPanel = this.getPanelForTab(tabToActivate); const tabPanel = this.getPanelForTab(tabToActivate);
tabPanel.classList.add(ACTIVE_PANEL_CLASS); tabPanel.classList.add(ACTIVE_PANEL_CLASS);
if (this.history === HISTORY_TYPE_HASH) historyReplaceState(tabToActivate.getAttribute('href'));
this.activeTab = tabToActivate; this.activeTab = tabToActivate;
this.dispatchTabShown(tabToActivate, tabPanel); this.dispatchTabShown(tabToActivate, tabPanel);

View File

@ -248,7 +248,7 @@ export default {
labels: this.enableAutocomplete, labels: this.enableAutocomplete,
snippets: this.enableAutocomplete, snippets: this.enableAutocomplete,
vulnerabilities: this.enableAutocomplete, vulnerabilities: this.enableAutocomplete,
contacts: this.enableAutocomplete && this.glFeatures.contactsAutocomplete, contacts: this.enableAutocomplete,
}, },
true, true,
); );

View File

@ -41,7 +41,6 @@ class Projects::IssuesController < Projects::ApplicationController
before_action :authorize_download_code!, only: [:related_branches] before_action :authorize_download_code!, only: [:related_branches]
before_action do before_action do
push_frontend_feature_flag(:contacts_autocomplete, project&.group)
push_frontend_feature_flag(:incident_timeline, project) push_frontend_feature_flag(:incident_timeline, project)
end end

View File

@ -15,7 +15,7 @@ module UsersHelper
end end
def user_email_help_text(user) def user_email_help_text(user)
return 'We also use email for avatar detection if no avatar is uploaded' unless user.unconfirmed_email.present? return 'We also use email for avatar detection if no avatar is uploaded.' unless user.unconfirmed_email.present?
confirmation_link = link_to 'Resend confirmation e-mail', user_confirmation_path(user: { email: @user.unconfirmed_email }), method: :post confirmation_link = link_to 'Resend confirmation e-mail', user_confirmation_path(user: { email: @user.unconfirmed_email }), method: :post

View File

@ -47,7 +47,7 @@ module BulkImports
end end
def non_patch_source_version def non_patch_source_version
Gitlab::VersionInfo.new(source_version.major, source_version.minor, 0) source_version.without_patch
end end
def log_skipped_pipeline(pipeline, minimum_version, maximum_version) def log_skipped_pipeline(pipeline, minimum_version, maximum_version)

View File

@ -13,6 +13,7 @@ module Issues
# in the caller (for example, an issue created via email) and the required arguments to the # in the caller (for example, an issue created via email) and the required arguments to the
# SpamParams constructor are not otherwise available, spam_params: must be explicitly passed as nil. # SpamParams constructor are not otherwise available, spam_params: must be explicitly passed as nil.
def initialize(project:, current_user: nil, params: {}, spam_params:, build_service: nil) def initialize(project:, current_user: nil, params: {}, spam_params:, build_service: nil)
@extra_params = params.delete(:extra_params) || {}
super(project: project, current_user: current_user, params: params) super(project: project, current_user: current_user, params: params)
@spam_params = spam_params @spam_params = spam_params
@build_service = build_service || BuildService.new(project: project, current_user: current_user, params: params) @build_service = build_service || BuildService.new(project: project, current_user: current_user, params: params)
@ -56,7 +57,7 @@ module Issues
handle_add_related_issue(issue) handle_add_related_issue(issue)
resolve_discussions_with_issue(issue) resolve_discussions_with_issue(issue)
create_escalation_status(issue) create_escalation_status(issue)
try_to_associate_contact(issue) try_to_associate_contacts(issue)
super super
end end
@ -85,7 +86,7 @@ module Issues
private private
attr_reader :spam_params attr_reader :spam_params, :extra_params
def create_escalation_status(issue) def create_escalation_status(issue)
::IncidentManagement::IssuableEscalationStatuses::CreateService.new(issue).execute if issue.supports_escalation? ::IncidentManagement::IssuableEscalationStatuses::CreateService.new(issue).execute if issue.supports_escalation?
@ -101,11 +102,14 @@ module Issues
IssueLinks::CreateService.new(issue, issue.author, { target_issuable: @add_related_issue }).execute IssueLinks::CreateService.new(issue, issue.author, { target_issuable: @add_related_issue }).execute
end end
def try_to_associate_contact(issue) def try_to_associate_contacts(issue)
return unless issue.external_author return unless issue.external_author
return unless current_user.can?(:set_issue_crm_contacts, issue) return unless current_user.can?(:set_issue_crm_contacts, issue)
set_crm_contacts(issue, [issue.external_author]) contacts = [issue.external_author]
contacts.concat extra_params[:cc] unless extra_params[:cc].nil?
set_crm_contacts(issue, contacts)
end end
end end
end end

View File

@ -1,6 +1,6 @@
= form_for @group, html: { multipart: true, class: 'gl-show-field-errors js-general-settings-form' }, authenticity_token: true do |f| = form_for @group, html: { multipart: true, class: 'gl-show-field-errors js-general-settings-form' }, authenticity_token: true do |f|
%input{ type: 'hidden', name: 'update_section', value: 'js-general-settings' } %input{ type: 'hidden', name: 'update_section', value: 'js-general-settings' }
= form_errors(@group) = form_errors(@group, pajamas_alert: true)
%fieldset %fieldset
.row .row

View File

@ -1,10 +1,7 @@
- to_names = content_tag(:strong, issuable.assignees.any? ? sanitize_name(issuable.assignee_list) : s_('Unassigned'))
%p %p
Assignee changed
- if previous_assignees.any? - if previous_assignees.any?
from = html_escape(s_('Notify|Assignee changed from %{fromNames} to %{toNames}').html_safe % { fromNames: content_tag(:strong, sanitize_name(previous_assignees.map(&:name).to_sentence)), toNames: to_names })
%strong= sanitize_name(previous_assignees.map(&:name).to_sentence)
to
- if issuable.assignees.any?
%strong= sanitize_name(issuable.assignee_list)
- else - else
%strong Unassigned = html_escape(s_('Notify|Assignee changed to %{toNames}').html_safe % { toNames: to_names})

View File

@ -1,3 +1,2 @@
%p %p
#{'Label'.pluralize(@label_names.size)} added: = html_escape(n_('Label added: %{labels}', 'Labels added: %{labels}', @label_names.size).html_safe % { labels: content_tag(:em, @label_names.to_sentence).html_safe })
%em= @label_names.to_sentence

View File

@ -22,12 +22,12 @@
{ include_blank: s_("Profiles|Do not show on profile") }, { include_blank: s_("Profiles|Do not show on profile") },
{ class: 'gl-form-select custom-select', disabled: email_change_disabled } { class: 'gl-form-select custom-select', disabled: email_change_disabled }
%small.form-text.text-gl-muted %small.form-text.text-gl-muted
= s_("Profiles|This email will be displayed on your public profile") = s_("Profiles|This email will be displayed on your public profile.")
.form-group.gl-form-group .form-group.gl-form-group
- commit_email_link_url = help_page_path('user/profile/index', anchor: 'change-the-email-displayed-on-your-commits', target: '_blank') - commit_email_link_url = help_page_path('user/profile/index', anchor: 'change-the-email-displayed-on-your-commits', target: '_blank')
- commit_email_link_start = '<a href="%{url}">'.html_safe % { url: commit_email_link_url } - commit_email_link_start = '<a href="%{url}">'.html_safe % { url: commit_email_link_url }
- commit_email_docs_link = s_('Profiles|This email will be used for web based operations, such as edits and merges. %{commit_email_link_start}Learn more%{commit_email_link_end}').html_safe % { commit_email_link_start: commit_email_link_start, commit_email_link_end: '</a>'.html_safe } - commit_email_docs_link = s_('Profiles|This email will be used for web based operations, such as edits and merges. %{commit_email_link_start}Learn more.%{commit_email_link_end}').html_safe % { commit_email_link_start: commit_email_link_start, commit_email_link_end: '</a>'.html_safe }
= form.label :commit_email, s_('Profiles|Commit email') = form.label :commit_email, s_('Profiles|Commit email')
.gl-md-form-input-lg .gl-md-form-input-lg
= form.select :commit_email, = form.select :commit_email,

View File

@ -2,8 +2,8 @@
- if user.read_only_attribute?(:name) - if user.read_only_attribute?(:name)
= form.text_field :name, class: 'gl-form-input form-control', required: true, readonly: true = form.text_field :name, class: 'gl-form-input form-control', required: true, readonly: true
%small.form-text.text-gl-muted %small.form-text.text-gl-muted
= s_("Profiles|Your name was automatically set based on your %{provider_label} account, so people you know can recognize you") % { provider_label: attribute_provider_label(:name) } = s_("Profiles|Your name was automatically set based on your %{provider_label} account, so people you know can recognize you.") % { provider_label: attribute_provider_label(:name) }
- else - else
= form.text_field :name, class: 'gl-form-input form-control', required: true, title: s_("Profiles|Using emojis in names seems fun, but please try to set a status message instead") = form.text_field :name, class: 'gl-form-input form-control', required: true, title: s_("Profiles|Using emojis in names seems fun, but please try to set a status message instead")
%small.form-text.text-gl-muted %small.form-text.text-gl-muted
= s_("Profiles|Enter your name, so people you know can recognize you") = s_("Profiles|Enter your name, so people you know can recognize you.")

View File

@ -74,7 +74,7 @@
.form-group.gl-form-group .form-group.gl-form-group
= status_form.gitlab_ui_checkbox_component :availability, = status_form.gitlab_ui_checkbox_component :availability,
s_("Profiles|Busy"), s_("Profiles|Busy"),
help_text: s_('Profiles|An indicator appears next to your name and avatar'), help_text: s_('Profiles|An indicator appears next to your name and avatar.'),
checkbox_options: { data: { testid: "user-availability-checkbox" } }, checkbox_options: { data: { testid: "user-availability-checkbox" } },
checked_value: availability["busy"], checked_value: availability["busy"],
unchecked_value: availability["not_set"] unchecked_value: availability["not_set"]
@ -83,7 +83,7 @@
.row.user-time-preferences.js-search-settings-section .row.user-time-preferences.js-search-settings-section
.col-lg-4.profile-settings-sidebar .col-lg-4.profile-settings-sidebar
%h4.gl-mt-0= s_("Profiles|Time settings") %h4.gl-mt-0= s_("Profiles|Time settings")
%p= s_("Profiles|Set your local time zone") %p= s_("Profiles|Set your local time zone.")
.col-lg-8 .col-lg-8
%h5= _("Time zone") %h5= _("Time zone")
= dropdown_tag(_("Select a time zone"), options: { toggle_class: 'gl-button btn js-timezone-dropdown input-lg gl-w-full!', title: _("Select a time zone"), filter: true, placeholder: s_("OfSearchInADropdown|Filter"), data: { data: timezone_data } } ) = dropdown_tag(_("Select a time zone"), options: { toggle_class: 'gl-button btn js-timezone-dropdown input-lg gl-w-full!', title: _("Select a time zone"), filter: true, placeholder: s_("OfSearchInADropdown|Filter"), data: { data: timezone_data } } )
@ -95,7 +95,7 @@
%h4.gl-mt-0 %h4.gl-mt-0
= s_("Profiles|Main settings") = s_("Profiles|Main settings")
%p %p
= s_("Profiles|This information will appear on your profile") = s_("Profiles|This information will appear on your profile.")
- if current_user.ldap_user? - if current_user.ldap_user?
= s_("Profiles|Some options are unavailable for LDAP accounts") = s_("Profiles|Some options are unavailable for LDAP accounts")
.col-lg-8 .col-lg-8
@ -109,12 +109,12 @@
= f.label :pronouns, s_('Profiles|Pronouns') = f.label :pronouns, s_('Profiles|Pronouns')
= f.text_field :pronouns, class: 'gl-form-input form-control gl-md-form-input-lg' = f.text_field :pronouns, class: 'gl-form-input form-control gl-md-form-input-lg'
%small.form-text.text-gl-muted %small.form-text.text-gl-muted
= s_("Profiles|Enter your pronouns to let people know how to refer to you") = s_("Profiles|Enter your pronouns to let people know how to refer to you.")
.form-group.gl-form-group .form-group.gl-form-group
= f.label :pronunciation, s_('Profiles|Pronunciation') = f.label :pronunciation, s_('Profiles|Pronunciation')
= f.text_field :pronunciation, class: 'gl-form-input form-control gl-md-form-input-lg' = f.text_field :pronunciation, class: 'gl-form-input form-control gl-md-form-input-lg'
%small.form-text.text-gl-muted %small.form-text.text-gl-muted
= s_("Profiles|Enter how your name is pronounced to help people address you correctly") = s_("Profiles|Enter how your name is pronounced to help people address you correctly.")
= render_if_exists 'profiles/extra_settings', form: f = render_if_exists 'profiles/extra_settings', form: f
= render_if_exists 'profiles/email_settings', form: f = render_if_exists 'profiles/email_settings', form: f
.form-group.gl-form-group .form-group.gl-form-group
@ -146,17 +146,17 @@
= f.label :organization, s_('Profiles|Organization') = f.label :organization, s_('Profiles|Organization')
= f.text_field :organization, class: 'gl-form-input form-control gl-md-form-input-lg' = f.text_field :organization, class: 'gl-form-input form-control gl-md-form-input-lg'
%small.form-text.text-gl-muted %small.form-text.text-gl-muted
= s_("Profiles|Who you represent or work for") = s_("Profiles|Who you represent or work for.")
.form-group.gl-form-group .form-group.gl-form-group
= f.label :bio, s_('Profiles|Bio') = f.label :bio, s_('Profiles|Bio')
= f.text_area :bio, class: 'gl-form-input gl-form-textarea form-control', rows: 4, maxlength: 250 = f.text_area :bio, class: 'gl-form-input gl-form-textarea form-control', rows: 4, maxlength: 250
%small.form-text.text-gl-muted %small.form-text.text-gl-muted
= s_("Profiles|Tell us about yourself in fewer than 250 characters") = s_("Profiles|Tell us about yourself in fewer than 250 characters.")
%hr %hr
%fieldset.form-group.gl-form-group %fieldset.form-group.gl-form-group
%legend.col-form-label.col-form-label %legend.col-form-label.col-form-label
= _('Private profile') = _('Private profile')
- private_profile_label = s_("Profiles|Don't display activity-related personal information on your profile") - private_profile_label = s_("Profiles|Don't display activity-related personal information on your profile.")
- private_profile_help_link = link_to sprite_icon('question-o'), help_page_path('user/profile/index.md', anchor: 'make-your-user-profile-page-private') - private_profile_help_link = link_to sprite_icon('question-o'), help_page_path('user/profile/index.md', anchor: 'make-your-user-profile-page-private')
= f.gitlab_ui_checkbox_component :private_profile, '%{private_profile_label} %{private_profile_help_link}'.html_safe % { private_profile_label: private_profile_label, private_profile_help_link: private_profile_help_link.html_safe } = f.gitlab_ui_checkbox_component :private_profile, '%{private_profile_label} %{private_profile_help_link}'.html_safe % { private_profile_label: private_profile_label, private_profile_help_link: private_profile_help_link.html_safe }
%fieldset.form-group.gl-form-group %fieldset.form-group.gl-form-group
@ -164,7 +164,7 @@
= s_("Profiles|Private contributions") = s_("Profiles|Private contributions")
= f.gitlab_ui_checkbox_component :include_private_contributions, = f.gitlab_ui_checkbox_component :include_private_contributions,
s_('Profiles|Include private contributions on my profile'), s_('Profiles|Include private contributions on my profile'),
help_text: s_("Profiles|Choose to show contributions of private projects on your public profile without any project, repository or organization information") help_text: s_("Profiles|Choose to show contributions of private projects on your public profile without any project, repository or organization information.")
%hr %hr
= f.submit s_("Profiles|Update profile settings"), class: 'gl-button btn btn-confirm gl-mr-3 js-password-prompt-btn' = f.submit s_("Profiles|Update profile settings"), class: 'gl-button btn btn-confirm gl-mr-3 js-password-prompt-btn'
= link_to _("Cancel"), user_path(current_user), class: 'gl-button btn btn-default btn-cancel' = link_to _("Cancel"), user_path(current_user), class: 'gl-button btn btn-default btn-cancel'

View File

@ -1,8 +0,0 @@
---
name: contacts_autocomplete
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/79639
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/352123
milestone: '14.8'
type: development
group: group::product planning
default_enabled: true

View File

@ -221,20 +221,6 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
end end
end end
# Legacy routes for `/-/integrations` which are now in `/-/settings/integrations`.
# Can be removed in 15.2, see https://gitlab.com/gitlab-org/gitlab/-/issues/334846
resources :integrations, controller: 'settings/integrations', constraints: { id: %r{[^/]+} }, only: [:edit, :update] do
member do
put :test
end
resources :hook_logs, only: [:show], controller: 'settings/integration_hook_logs' do
member do
post :retry
end
end
end
resources :boards, only: [:index, :show, :create, :update, :destroy], constraints: { id: /\d+/ } do resources :boards, only: [:index, :show, :create, :update, :destroy], constraints: { id: /\d+/ } do
collection do collection do
get :recent get :recent

View File

@ -66,7 +66,8 @@ Supported attributes:
| `attribute` | datatype | **{dotted-circle}** No | Detailed description. | | `attribute` | datatype | **{dotted-circle}** No | Detailed description. |
| `attribute` | datatype | **{dotted-circle}** No | Detailed description. | | `attribute` | datatype | **{dotted-circle}** No | Detailed description. |
Response body attributes: If successful, returns [`<status_code>`](../../api/index.md#status-codes) and the following
response attributes:
| Attribute | Type | Description | | Attribute | Type | Description |
|:-------------------------|:---------|:----------------------| |:-------------------------|:---------|:----------------------|
@ -151,6 +152,14 @@ For information about writing attribute descriptions, see the [GraphQL API descr
## Response body description ## Response body description
Start the description with the following sentence, replacing `status code` with the
relevant [HTTP status code](../../api/index.md#status-codes), for example:
```markdown
If successful, returns [`200 OK`](../../api/index.md#status-codes) and the
following response attributes:
```
Use the following table headers to describe the response bodies. Attributes should Use the following table headers to describe the response bodies. Attributes should
always be in code blocks using backticks (`` ` ``). always be in code blocks using backticks (`` ` ``).

View File

@ -118,6 +118,9 @@ organizations using the GraphQL API.
## Issues ## Issues
If you use [Service Desk](../project/service_desk.md) and create issues from emails,
issues are linked to contacts matching the email addresses in the sender and CC of the email.
### View issues linked to a contact ### View issues linked to a contact
To view a contact's issues, select a contact from the issue sidebar, or: To view a contact's issues, select a contact from the issue sidebar, or:
@ -170,10 +173,7 @@ API.
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/2256) in GitLab 14.8 [with a flag](../../administration/feature_flags.md) named `contacts_autocomplete`. Disabled by default. > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/2256) in GitLab 14.8 [with a flag](../../administration/feature_flags.md) named `contacts_autocomplete`. Disabled by default.
> - [Enabled on GitLab.com and self-managed](https://gitlab.com/gitlab-org/gitlab/-/issues/352123) in GitLab 15.0. > - [Enabled on GitLab.com and self-managed](https://gitlab.com/gitlab-org/gitlab/-/issues/352123) in GitLab 15.0.
> - [Generally available](https://gitlab.com/gitlab-org/gitlab/-/issues/352123) in GitLab 15.2. [Feature flag `contacts_autocomplete`](https://gitlab.com/gitlab-org/gitlab/-/issues/352123) removed.
FLAG:
On self-managed GitLab, by default this feature is available. To hide the feature, ask an administrator to [disable the feature flag](../../administration/feature_flags.md) named `contacts_autocomplete`.
On GitLab.com, this feature is available.
When you use the `/add_contacts` or `/remove_contacts` quick actions, follow them with `[contact:` and an autocomplete list appears: When you use the `/add_contacts` or `/remove_contacts` quick actions, follow them with `[contact:` and an autocomplete list appears:

View File

@ -96,8 +96,8 @@ module Backup
def build_env def build_env
{ {
'SSL_CERT_FILE' => OpenSSL::X509::DEFAULT_CERT_FILE, 'SSL_CERT_FILE' => Gitlab::X509::Certificate.default_cert_file,
'SSL_CERT_DIR' => OpenSSL::X509::DEFAULT_CERT_DIR 'SSL_CERT_DIR' => Gitlab::X509::Certificate.default_cert_dir
}.merge(ENV) }.merge(ENV)
end end

View File

@ -25,18 +25,16 @@ module Gitlab
raise ArgumentError, "'#{orig_runner_version}' is not a valid version" unless runner_version.valid? raise ArgumentError, "'#{orig_runner_version}' is not a valid version" unless runner_version.valid?
gitlab_minor_version = version_without_patch(@gitlab_version) gitlab_minor_version = @gitlab_version.without_patch
available_releases = releases available_releases = releases
.reject { |release| release.major > @gitlab_version.major } .reject { |release| release.major > @gitlab_version.major }
.reject do |release| .reject do |release|
release_minor_version = version_without_patch(release) # Do not reject a patch update, even if the runner is ahead of the instance version
next false if release.same_minor_version?(runner_version)
# Do not reject a patch update, even if the runner is ahead of the instance version release.without_patch > gitlab_minor_version
next false if version_without_patch(runner_version) == release_minor_version end
release_minor_version > gitlab_minor_version
end
return :recommended if available_releases.any? { |available_rel| patch_update?(available_rel, runner_version) } return :recommended if available_releases.any? { |available_rel| patch_update?(available_rel, runner_version) }
return :recommended if outside_backport_window?(runner_version, releases) return :recommended if outside_backport_window?(runner_version, releases)
@ -63,19 +61,15 @@ module Gitlab
def outside_backport_window?(runner_version, releases) def outside_backport_window?(runner_version, releases)
return false if runner_version >= releases.last # return early if runner version is too new return false if runner_version >= releases.last # return early if runner version is too new
latest_minor_releases = releases.map { |r| version_without_patch(r) }.uniq { |v| v.to_s } latest_minor_releases = releases.map(&:without_patch).uniq
latest_version_position = latest_minor_releases.count - 1 latest_version_position = latest_minor_releases.count - 1
runner_version_position = latest_minor_releases.index(version_without_patch(runner_version)) runner_version_position = latest_minor_releases.index(runner_version.without_patch)
return true if runner_version_position.nil? # consider outside if version is too old return true if runner_version_position.nil? # consider outside if version is too old
# https://docs.gitlab.com/ee/policy/maintenance.html#backporting-to-older-releases # https://docs.gitlab.com/ee/policy/maintenance.html#backporting-to-older-releases
latest_version_position - runner_version_position > 2 latest_version_position - runner_version_position > 2
end end
def version_without_patch(version)
::Gitlab::VersionInfo.new(version.major, version.minor, 0)
end
end end
end end
end end

View File

@ -98,7 +98,10 @@ module Gitlab
title: mail.subject, title: mail.subject,
description: message_including_template, description: message_including_template,
confidential: true, confidential: true,
external_author: from_address external_author: from_address,
extra_params: {
cc: mail.cc
}
}, },
spam_params: nil spam_params: nil
).execute ).execute

View File

@ -52,5 +52,21 @@ module Gitlab
def valid? def valid?
@major >= 0 && @minor >= 0 && @patch >= 0 && @major + @minor + @patch > 0 @major >= 0 && @minor >= 0 && @patch >= 0 && @major + @minor + @patch > 0
end end
def hash
[self.class, to_s].hash
end
def eql?(other)
(self <=> other) == 0
end
def same_minor_version?(other)
@major == other.major && @minor == other.minor
end
def without_patch
self.class.new(@major, @minor, 0)
end
end end
end end

View File

@ -23,6 +23,18 @@ module Gitlab
include ::Gitlab::Utils::StrongMemoize include ::Gitlab::Utils::StrongMemoize
end end
def self.default_cert_dir
strong_memoize(:default_cert_dir) do
ENV.fetch('SSL_CERT_DIR', OpenSSL::X509::DEFAULT_CERT_DIR)
end
end
def self.default_cert_file
strong_memoize(:default_cert_file) do
ENV.fetch('SSL_CERT_FILE', OpenSSL::X509::DEFAULT_CERT_FILE)
end
end
def self.from_strings(key_string, cert_string, ca_certs_string = nil) def self.from_strings(key_string, cert_string, ca_certs_string = nil)
key = OpenSSL::PKey::RSA.new(key_string) key = OpenSSL::PKey::RSA.new(key_string)
cert = OpenSSL::X509::Certificate.new(cert_string) cert = OpenSSL::X509::Certificate.new(cert_string)
@ -39,10 +51,10 @@ module Gitlab
# Returns all top-level, readable files in the default CA cert directory # Returns all top-level, readable files in the default CA cert directory
def self.ca_certs_paths def self.ca_certs_paths
cert_paths = Dir["#{OpenSSL::X509::DEFAULT_CERT_DIR}/*"].select do |path| cert_paths = Dir["#{default_cert_dir}/*"].select do |path|
!File.directory?(path) && File.readable?(path) !File.directory?(path) && File.readable?(path)
end end
cert_paths << OpenSSL::X509::DEFAULT_CERT_FILE if File.exist? OpenSSL::X509::DEFAULT_CERT_FILE cert_paths << default_cert_file if File.exist? default_cert_file
cert_paths cert_paths
end end
@ -61,6 +73,11 @@ module Gitlab
clear_memoization(:ca_certs_bundle) clear_memoization(:ca_certs_bundle)
end end
def self.reset_default_cert_paths
clear_memoization(:default_cert_dir)
clear_memoization(:default_cert_file)
end
# Returns an array of OpenSSL::X509::Certificate objects, empty array if none found # Returns an array of OpenSSL::X509::Certificate objects, empty array if none found
# #
# Ruby OpenSSL::X509::Certificate.new will only load the first # Ruby OpenSSL::X509::Certificate.new will only load the first

View File

@ -59,7 +59,7 @@ module Gitlab
if Feature.enabled?(:x509_forced_cert_loading, type: :ops) if Feature.enabled?(:x509_forced_cert_loading, type: :ops)
# Forcibly load the default cert file because the OpenSSL library seemingly ignores it # Forcibly load the default cert file because the OpenSSL library seemingly ignores it
store.add_file(OpenSSL::X509::DEFAULT_CERT_FILE) if File.exist?(OpenSSL::X509::DEFAULT_CERT_FILE) store.add_file(Gitlab::X509::Certificate.default_cert_file) if File.exist?(Gitlab::X509::Certificate.default_cert_file) # rubocop:disable Layout/LineLength
end end
# valid_signing_time? checks the time attributes already # valid_signing_time? checks the time attributes already

View File

@ -22411,6 +22411,11 @@ msgstr ""
msgid "Label actions dropdown" msgid "Label actions dropdown"
msgstr "" msgstr ""
msgid "Label added: %{labels}"
msgid_plural "Labels added: %{labels}"
msgstr[0] ""
msgstr[1] ""
msgid "Label priority" msgid "Label priority"
msgstr "" msgstr ""
@ -26188,6 +26193,12 @@ msgstr ""
msgid "Notify|%{author_link}'s issue %{issue_reference_link} is due soon." msgid "Notify|%{author_link}'s issue %{issue_reference_link} is due soon."
msgstr "" msgstr ""
msgid "Notify|Assignee changed from %{fromNames} to %{toNames}"
msgstr ""
msgid "Notify|Assignee changed to %{toNames}"
msgstr ""
msgid "Notify|Author: %{author_name}" msgid "Notify|Author: %{author_name}"
msgstr "" msgstr ""
@ -29204,7 +29215,7 @@ msgstr ""
msgid "Profiles|An error occurred while updating your username, please try again." msgid "Profiles|An error occurred while updating your username, please try again."
msgstr "" msgstr ""
msgid "Profiles|An indicator appears next to your name and avatar" msgid "Profiles|An indicator appears next to your name and avatar."
msgstr "" msgstr ""
msgid "Profiles|Avatar cropper" msgid "Profiles|Avatar cropper"
@ -29231,7 +29242,7 @@ msgstr ""
msgid "Profiles|Choose file..." msgid "Profiles|Choose file..."
msgstr "" msgstr ""
msgid "Profiles|Choose to show contributions of private projects on your public profile without any project, repository or organization information" msgid "Profiles|Choose to show contributions of private projects on your public profile without any project, repository or organization information."
msgstr "" msgstr ""
msgid "Profiles|City, country" msgid "Profiles|City, country"
@ -29273,7 +29284,7 @@ msgstr ""
msgid "Profiles|Do not show on profile" msgid "Profiles|Do not show on profile"
msgstr "" msgstr ""
msgid "Profiles|Don't display activity-related personal information on your profile" msgid "Profiles|Don't display activity-related personal information on your profile."
msgstr "" msgstr ""
msgid "Profiles|Edit Profile" msgid "Profiles|Edit Profile"
@ -29282,16 +29293,16 @@ msgstr ""
msgid "Profiles|Ensure you have two-factor authentication recovery codes stored in a safe place." msgid "Profiles|Ensure you have two-factor authentication recovery codes stored in a safe place."
msgstr "" msgstr ""
msgid "Profiles|Enter how your name is pronounced to help people address you correctly" msgid "Profiles|Enter how your name is pronounced to help people address you correctly."
msgstr "" msgstr ""
msgid "Profiles|Enter your name, so people you know can recognize you" msgid "Profiles|Enter your name, so people you know can recognize you."
msgstr "" msgstr ""
msgid "Profiles|Enter your password to confirm the email change" msgid "Profiles|Enter your password to confirm the email change"
msgstr "" msgstr ""
msgid "Profiles|Enter your pronouns to let people know how to refer to you" msgid "Profiles|Enter your pronouns to let people know how to refer to you."
msgstr "" msgstr ""
msgid "Profiles|Example: MacBook key" msgid "Profiles|Example: MacBook key"
@ -29414,7 +29425,7 @@ msgstr ""
msgid "Profiles|Set new profile picture" msgid "Profiles|Set new profile picture"
msgstr "" msgstr ""
msgid "Profiles|Set your local time zone" msgid "Profiles|Set your local time zone."
msgstr "" msgstr ""
msgid "Profiles|Social sign-in" msgid "Profiles|Social sign-in"
@ -29426,7 +29437,7 @@ msgstr ""
msgid "Profiles|Static object token was successfully reset" msgid "Profiles|Static object token was successfully reset"
msgstr "" msgstr ""
msgid "Profiles|Tell us about yourself in fewer than 250 characters" msgid "Profiles|Tell us about yourself in fewer than 250 characters."
msgstr "" msgstr ""
msgid "Profiles|The ability to update your name has been disabled by your administrator." msgid "Profiles|The ability to update your name has been disabled by your administrator."
@ -29435,16 +29446,16 @@ msgstr ""
msgid "Profiles|The maximum file size allowed is 200KB." msgid "Profiles|The maximum file size allowed is 200KB."
msgstr "" msgstr ""
msgid "Profiles|This email will be displayed on your public profile" msgid "Profiles|This email will be displayed on your public profile."
msgstr "" msgstr ""
msgid "Profiles|This email will be used for web based operations, such as edits and merges. %{commit_email_link_start}Learn more%{commit_email_link_end}" msgid "Profiles|This email will be used for web based operations, such as edits and merges. %{commit_email_link_start}Learn more.%{commit_email_link_end}"
msgstr "" msgstr ""
msgid "Profiles|This emoji and message will appear on your profile and throughout the interface." msgid "Profiles|This emoji and message will appear on your profile and throughout the interface."
msgstr "" msgstr ""
msgid "Profiles|This information will appear on your profile" msgid "Profiles|This information will appear on your profile."
msgstr "" msgstr ""
msgid "Profiles|Time settings" msgid "Profiles|Time settings"
@ -29489,7 +29500,7 @@ msgstr ""
msgid "Profiles|What's your status?" msgid "Profiles|What's your status?"
msgstr "" msgstr ""
msgid "Profiles|Who you represent or work for" msgid "Profiles|Who you represent or work for."
msgstr "" msgstr ""
msgid "Profiles|You can change your avatar here" msgid "Profiles|You can change your avatar here"
@ -29531,6 +29542,9 @@ msgstr ""
msgid "Profiles|Your name was automatically set based on your %{provider_label} account, so people you know can recognize you" msgid "Profiles|Your name was automatically set based on your %{provider_label} account, so people you know can recognize you"
msgstr "" msgstr ""
msgid "Profiles|Your name was automatically set based on your %{provider_label} account, so people you know can recognize you."
msgstr ""
msgid "Profiles|Your status" msgid "Profiles|Your status"
msgstr "" msgstr ""

View File

@ -6,6 +6,7 @@ Received: by 10.0.0.1 with HTTP; Thu, 13 Jun 2013 14:03:48 -0700
Date: Thu, 13 Jun 2013 17:03:48 -0400 Date: Thu, 13 Jun 2013 17:03:48 -0400
From: Jake the Dog <jake@adventuretime.ooo> From: Jake the Dog <jake@adventuretime.ooo>
To: incoming+email-test-project_id-issue-@appmail.adventuretime.ooo To: incoming+email-test-project_id-issue-@appmail.adventuretime.ooo
Cc: Carbon Copy <cc@example.com>, kk@example.org
Message-ID: <CADkmRc+rNGAGGbV2iE5p918UVy4UyJqVcXRO2=otppgzduJSg@mail.gmail.com> Message-ID: <CADkmRc+rNGAGGbV2iE5p918UVy4UyJqVcXRO2=otppgzduJSg@mail.gmail.com>
Subject: The message subject! @all Subject: The message subject! @all
Mime-Version: 1.0 Mime-Version: 1.0

View File

@ -1,8 +1,15 @@
import Vue, { nextTick } from 'vue'; import Vue, { nextTick } from 'vue';
import Vuex from 'vuex'; import Vuex from 'vuex';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { TEST_HOST } from 'helpers/test_constants';
import { visitUrl } from '~/lib/utils/url_utility';
import PreviewDropdown from '~/batch_comments/components/preview_dropdown.vue'; import PreviewDropdown from '~/batch_comments/components/preview_dropdown.vue';
jest.mock('~/lib/utils/url_utility', () => ({
visitUrl: jest.fn(),
setUrlParams: jest.requireActual('~/lib/utils/url_utility').setUrlParams,
}));
Vue.use(Vuex); Vue.use(Vuex);
let wrapper; let wrapper;
@ -27,6 +34,11 @@ function factory({ viewDiffsFileByFile = false, draftsCount = 1, sortedDrafts =
actions: { scrollToDraft }, actions: { scrollToDraft },
getters: { draftsCount: () => draftsCount, sortedDrafts: () => sortedDrafts }, getters: { draftsCount: () => draftsCount, sortedDrafts: () => sortedDrafts },
}, },
notes: {
getters: {
getNoteableData: () => ({ diff_head_sha: '123' }),
},
},
}, },
}); });
@ -67,5 +79,19 @@ describe('Batch comments preview dropdown', () => {
expect(scrollToDraft).toHaveBeenCalledWith(expect.anything(), { id: 1 }); expect(scrollToDraft).toHaveBeenCalledWith(expect.anything(), { id: 1 });
}); });
it('changes window location to navigate to commit', async () => {
factory({
viewDiffsFileByFile: false,
sortedDrafts: [{ id: 1, position: { head_sha: '1234' } }],
});
wrapper.findByTestId('preview-item').vm.$emit('click');
await nextTick();
expect(scrollToDraft).not.toHaveBeenCalled();
expect(visitUrl).toHaveBeenCalledWith(`${TEST_HOST}/?commit_id=1234#note_1`);
});
}); });
}); });

View File

@ -88,6 +88,28 @@ describe('common_utils', () => {
expectGetElementIdToHaveBeenCalledWith('user-content-definição'); expectGetElementIdToHaveBeenCalledWith('user-content-definição');
}); });
it(`does not scroll when ${commonUtils.NO_SCROLL_TO_HASH_CLASS} is set on target`, () => {
jest.spyOn(window, 'scrollBy');
document.body.innerHTML += `
<div id="parent">
<a href="#test">Link</a>
<div style="height: 2000px;"></div>
<div id="test" style="height: 2000px;" class="${commonUtils.NO_SCROLL_TO_HASH_CLASS}"></div>
</div>
`;
window.history.pushState({}, null, '#test');
commonUtils.handleLocationHash();
jest.runOnlyPendingTimers();
try {
expect(window.scrollBy).not.toHaveBeenCalled();
} finally {
document.getElementById('parent').remove();
}
});
it('scrolls element into view', () => { it('scrolls element into view', () => {
document.body.innerHTML += ` document.body.innerHTML += `
<div id="parent"> <div id="parent">

View File

@ -1,9 +1,16 @@
import { GlTabsBehavior, TAB_SHOWN_EVENT } from '~/tabs'; import { GlTabsBehavior, TAB_SHOWN_EVENT, HISTORY_TYPE_HASH } from '~/tabs';
import { ACTIVE_PANEL_CLASS, ACTIVE_TAB_CLASSES } from '~/tabs/constants'; import { ACTIVE_PANEL_CLASS, ACTIVE_TAB_CLASSES } from '~/tabs/constants';
import { getLocationHash } from '~/lib/utils/url_utility';
import { NO_SCROLL_TO_HASH_CLASS } from '~/lib/utils/common_utils';
import { getFixture, setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures'; import { getFixture, setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import setWindowLocation from 'helpers/set_window_location_helper';
const tabsFixture = getFixture('tabs/tabs.html'); const tabsFixture = getFixture('tabs/tabs.html');
global.CSS = {
escape: (val) => val,
};
describe('GlTabsBehavior', () => { describe('GlTabsBehavior', () => {
let glTabs; let glTabs;
let tabShownEventSpy; let tabShownEventSpy;
@ -41,6 +48,7 @@ describe('GlTabsBehavior', () => {
}); });
expect(panel.classList.contains(ACTIVE_PANEL_CLASS)).toBe(true); expect(panel.classList.contains(ACTIVE_PANEL_CLASS)).toBe(true);
expect(panel.classList.contains(NO_SCROLL_TO_HASH_CLASS)).toBe(true);
}; };
const expectInactiveTabAndPanel = (name) => { const expectInactiveTabAndPanel = (name) => {
@ -67,6 +75,7 @@ describe('GlTabsBehavior', () => {
}); });
expect(panel.classList.contains(ACTIVE_PANEL_CLASS)).toBe(false); expect(panel.classList.contains(ACTIVE_PANEL_CLASS)).toBe(false);
expect(panel.classList.contains(NO_SCROLL_TO_HASH_CLASS)).toBe(true);
}; };
const expectGlTabShownEvent = (name) => { const expectGlTabShownEvent = (name) => {
@ -263,4 +272,98 @@ describe('GlTabsBehavior', () => {
expectInactiveTabAndPanel('foo'); expectInactiveTabAndPanel('foo');
}); });
}); });
describe('using history=hash', () => {
const defaultTab = 'foo';
let tab;
let tabsEl;
beforeEach(() => {
setHTMLFixture(tabsFixture);
tabsEl = findByTestId('tabs');
});
afterEach(() => {
glTabs.destroy();
resetHTMLFixture();
});
describe('when a hash exists onInit', () => {
beforeEach(() => {
tab = 'bar';
setWindowLocation(`http://foo.com/index#${tab}`);
glTabs = new GlTabsBehavior(tabsEl, { history: HISTORY_TYPE_HASH });
});
it('sets the active tab to the hash and preserves hash', () => {
expectActiveTabAndPanel(tab);
expect(getLocationHash()).toBe(tab);
});
});
describe('when a hash does not exist onInit', () => {
beforeEach(() => {
setWindowLocation(`http://foo.com/index`);
glTabs = new GlTabsBehavior(tabsEl, { history: HISTORY_TYPE_HASH });
});
it('sets the active tab to the first tab and sets hash', () => {
expectActiveTabAndPanel(defaultTab);
expect(getLocationHash()).toBe(defaultTab);
});
});
describe('clicking on an inactive tab', () => {
beforeEach(() => {
tab = 'qux';
setWindowLocation(`http://foo.com/index`);
glTabs = new GlTabsBehavior(tabsEl, { history: HISTORY_TYPE_HASH });
findTab(tab).click();
});
it('changes the tabs and updates the hash', () => {
expectInactiveTabAndPanel(defaultTab);
expectActiveTabAndPanel(tab);
expect(getLocationHash()).toBe(tab);
});
});
describe('keyboard navigation', () => {
const secondTab = 'bar';
beforeEach(() => {
setWindowLocation(`http://foo.com/index`);
glTabs = new GlTabsBehavior(tabsEl, { history: HISTORY_TYPE_HASH });
});
it.each(['ArrowRight', 'ArrowDown'])(
'pressing %s moves to next tab and updates hash',
(code) => {
expectActiveTabAndPanel(defaultTab);
triggerKeyDown(code, glTabs.activeTab);
expectInactiveTabAndPanel(defaultTab);
expectActiveTabAndPanel(secondTab);
expect(getLocationHash()).toBe(secondTab);
},
);
it.each(['ArrowLeft', 'ArrowUp'])(
'pressing %s moves to previous tab and updates hash',
(code) => {
// First, make the 2nd tab active
findTab(secondTab).click();
expectActiveTabAndPanel(secondTab);
triggerKeyDown(code, glTabs.activeTab);
expectInactiveTabAndPanel(secondTab);
expectActiveTabAndPanel(defaultTab);
expect(getLocationHash()).toBe(defaultTab);
},
);
});
});
}); });

View File

@ -67,11 +67,6 @@ describe('Markdown field component', () => {
enablePreview, enablePreview,
restrictedToolBarItems, restrictedToolBarItems,
}, },
provide: {
glFeatures: {
contactsAutocomplete: true,
},
},
}, },
); );
} }

View File

@ -16,8 +16,8 @@ RSpec.describe Backup::GitalyBackup do
let(:expected_env) do let(:expected_env) do
{ {
'SSL_CERT_FILE' => OpenSSL::X509::DEFAULT_CERT_FILE, 'SSL_CERT_FILE' => Gitlab::X509::Certificate.default_cert_file,
'SSL_CERT_DIR' => OpenSSL::X509::DEFAULT_CERT_DIR 'SSL_CERT_DIR' => Gitlab::X509::Certificate.default_cert_dir
}.merge(ENV) }.merge(ENV)
end end

View File

@ -52,14 +52,6 @@ RSpec.describe Gitlab::Email::Handler::ServiceDeskHandler do
expect(new_issue.issue_email_participants.first.email).to eq(author_email) expect(new_issue.issue_email_participants.first.email).to eq(author_email)
end end
it 'attaches existing CRM contact' do
contact = create(:contact, group: group, email: author_email)
receiver.execute
new_issue = Issue.last
expect(new_issue.issue_customer_relations_contacts.last.contact).to eq(contact)
end
it 'sends thank you email' do it 'sends thank you email' do
expect { receiver.execute }.to have_enqueued_job.on_queue('mailers') expect { receiver.execute }.to have_enqueued_job.on_queue('mailers')
end end
@ -77,6 +69,16 @@ RSpec.describe Gitlab::Email::Handler::ServiceDeskHandler do
context 'when everything is fine' do context 'when everything is fine' do
it_behaves_like 'a new issue request' it_behaves_like 'a new issue request'
it 'attaches existing CRM contacts' do
contact = create(:contact, group: group, email: author_email)
contact2 = create(:contact, group: group, email: "cc@example.com")
contact3 = create(:contact, group: group, email: "kk@example.org")
receiver.execute
new_issue = Issue.last
expect(new_issue.issue_customer_relations_contacts.map(&:contact)).to contain_exactly(contact, contact2, contact3)
end
context 'with legacy incoming email address' do context 'with legacy incoming email address' do
let(:email_raw) { fixture_file('emails/service_desk_legacy.eml') } let(:email_raw) { fixture_file('emails/service_desk_legacy.eml') }

View File

@ -1,6 +1,6 @@
# frozen_string_literal: true # frozen_string_literal: true
require 'spec_helper' require 'fast_spec_helper'
RSpec.describe 'Gitlab::VersionInfo' do RSpec.describe 'Gitlab::VersionInfo' do
before do before do
@ -13,7 +13,7 @@ RSpec.describe 'Gitlab::VersionInfo' do
@v2_0_0 = Gitlab::VersionInfo.new(2, 0, 0) @v2_0_0 = Gitlab::VersionInfo.new(2, 0, 0)
end end
context '>' do describe '>' do
it { expect(@v2_0_0).to be > @v1_1_0 } it { expect(@v2_0_0).to be > @v1_1_0 }
it { expect(@v1_1_0).to be > @v1_0_1 } it { expect(@v1_1_0).to be > @v1_0_1 }
it { expect(@v1_0_1).to be > @v1_0_0 } it { expect(@v1_0_1).to be > @v1_0_0 }
@ -21,12 +21,12 @@ RSpec.describe 'Gitlab::VersionInfo' do
it { expect(@v0_1_0).to be > @v0_0_1 } it { expect(@v0_1_0).to be > @v0_0_1 }
end end
context '>=' do describe '>=' do
it { expect(@v2_0_0).to be >= Gitlab::VersionInfo.new(2, 0, 0) } it { expect(@v2_0_0).to be >= Gitlab::VersionInfo.new(2, 0, 0) }
it { expect(@v2_0_0).to be >= @v1_1_0 } it { expect(@v2_0_0).to be >= @v1_1_0 }
end end
context '<' do describe '<' do
it { expect(@v0_0_1).to be < @v0_1_0 } it { expect(@v0_0_1).to be < @v0_1_0 }
it { expect(@v0_1_0).to be < @v1_0_0 } it { expect(@v0_1_0).to be < @v1_0_0 }
it { expect(@v1_0_0).to be < @v1_0_1 } it { expect(@v1_0_0).to be < @v1_0_1 }
@ -34,29 +34,29 @@ RSpec.describe 'Gitlab::VersionInfo' do
it { expect(@v1_1_0).to be < @v2_0_0 } it { expect(@v1_1_0).to be < @v2_0_0 }
end end
context '<=' do describe '<=' do
it { expect(@v0_0_1).to be <= Gitlab::VersionInfo.new(0, 0, 1) } it { expect(@v0_0_1).to be <= Gitlab::VersionInfo.new(0, 0, 1) }
it { expect(@v0_0_1).to be <= @v0_1_0 } it { expect(@v0_0_1).to be <= @v0_1_0 }
end end
context '==' do describe '==' do
it { expect(@v0_0_1).to eq(Gitlab::VersionInfo.new(0, 0, 1)) } it { expect(@v0_0_1).to eq(Gitlab::VersionInfo.new(0, 0, 1)) }
it { expect(@v0_1_0).to eq(Gitlab::VersionInfo.new(0, 1, 0)) } it { expect(@v0_1_0).to eq(Gitlab::VersionInfo.new(0, 1, 0)) }
it { expect(@v1_0_0).to eq(Gitlab::VersionInfo.new(1, 0, 0)) } it { expect(@v1_0_0).to eq(Gitlab::VersionInfo.new(1, 0, 0)) }
end end
context '!=' do describe '!=' do
it { expect(@v0_0_1).not_to eq(@v0_1_0) } it { expect(@v0_0_1).not_to eq(@v0_1_0) }
end end
context 'unknown' do describe '.unknown' do
it { expect(@unknown).not_to be @v0_0_1 } it { expect(@unknown).not_to be @v0_0_1 }
it { expect(@unknown).not_to be Gitlab::VersionInfo.new } it { expect(@unknown).not_to be Gitlab::VersionInfo.new }
it { expect {@unknown > @v0_0_1}.to raise_error(ArgumentError) } it { expect {@unknown > @v0_0_1}.to raise_error(ArgumentError) }
it { expect {@unknown < @v0_0_1}.to raise_error(ArgumentError) } it { expect {@unknown < @v0_0_1}.to raise_error(ArgumentError) }
end end
context 'parse' do describe '.parse' do
it { expect(Gitlab::VersionInfo.parse("1.0.0")).to eq(@v1_0_0) } it { expect(Gitlab::VersionInfo.parse("1.0.0")).to eq(@v1_0_0) }
it { expect(Gitlab::VersionInfo.parse("1.0.0.1")).to eq(@v1_0_0) } it { expect(Gitlab::VersionInfo.parse("1.0.0.1")).to eq(@v1_0_0) }
it { expect(Gitlab::VersionInfo.parse("1.0.0-ee")).to eq(@v1_0_0) } it { expect(Gitlab::VersionInfo.parse("1.0.0-ee")).to eq(@v1_0_0) }
@ -66,8 +66,37 @@ RSpec.describe 'Gitlab::VersionInfo' do
it { expect(Gitlab::VersionInfo.parse("git 1.0b1")).not_to be_valid } it { expect(Gitlab::VersionInfo.parse("git 1.0b1")).not_to be_valid }
end end
context 'to_s' do describe '.to_s' do
it { expect(@v1_0_0.to_s).to eq("1.0.0") } it { expect(@v1_0_0.to_s).to eq("1.0.0") }
it { expect(@unknown.to_s).to eq("Unknown") } it { expect(@unknown.to_s).to eq("Unknown") }
end end
describe '.hash' do
it { expect(Gitlab::VersionInfo.parse("1.0.0").hash).to eq(@v1_0_0.hash) }
it { expect(Gitlab::VersionInfo.parse("1.0.0.1").hash).to eq(@v1_0_0.hash) }
it { expect(Gitlab::VersionInfo.parse("1.0.1b1").hash).to eq(@v1_0_1.hash) }
end
describe '.eql?' do
it { expect(Gitlab::VersionInfo.parse("1.0.0").eql?(@v1_0_0)).to be_truthy }
it { expect(Gitlab::VersionInfo.parse("1.0.0.1").eql?(@v1_0_0)).to be_truthy }
it { expect(@v1_0_1.eql?(@v1_0_0)).to be_falsey }
it { expect(@v1_1_0.eql?(@v1_0_0)).to be_falsey }
it { expect(@v1_0_0.eql?(@v1_0_0)).to be_truthy }
it { expect([@v1_0_0, @v1_1_0, @v1_0_0].uniq).to eq [@v1_0_0, @v1_1_0] }
end
describe '.same_minor_version?' do
it { expect(@v0_1_0.same_minor_version?(@v0_0_1)).to be_falsey }
it { expect(@v1_0_1.same_minor_version?(@v1_0_0)).to be_truthy }
it { expect(@v1_0_0.same_minor_version?(@v1_0_1)).to be_truthy }
it { expect(@v1_1_0.same_minor_version?(@v1_0_0)).to be_falsey }
it { expect(@v2_0_0.same_minor_version?(@v1_0_0)).to be_falsey }
end
describe '.without_patch' do
it { expect(@v0_1_0.without_patch).to eq(@v0_1_0) }
it { expect(@v1_0_0.without_patch).to eq(@v1_0_0) }
it { expect(@v1_0_1.without_patch).to eq(@v1_0_0) }
end
end end

View File

@ -116,9 +116,69 @@ RSpec.describe Gitlab::X509::Certificate do
end end
end end
describe '.default_cert_dir' do
before do
described_class.reset_default_cert_paths
end
after(:context) do
described_class.reset_default_cert_paths
end
context 'when SSL_CERT_DIR env variable is not set' do
before do
stub_env('SSL_CERT_DIR', nil)
end
it 'returns default directory from OpenSSL' do
expect(described_class.default_cert_dir).to eq(OpenSSL::X509::DEFAULT_CERT_DIR)
end
end
context 'when SSL_CERT_DIR env variable is set' do
before do
stub_env('SSL_CERT_DIR', '/tmp/foo/certs')
end
it 'returns specified directory' do
expect(described_class.default_cert_dir).to eq('/tmp/foo/certs')
end
end
end
describe '.default_cert_file' do
before do
described_class.reset_default_cert_paths
end
after(:context) do
described_class.reset_default_cert_paths
end
context 'when SSL_CERT_FILE env variable is not set' do
before do
stub_env('SSL_CERT_FILE', nil)
end
it 'returns default file from OpenSSL' do
expect(described_class.default_cert_file).to eq(OpenSSL::X509::DEFAULT_CERT_FILE)
end
end
context 'when SSL_CERT_FILE env variable is set' do
before do
stub_env('SSL_CERT_FILE', '/tmp/foo/cert.pem')
end
it 'returns specified file' do
expect(described_class.default_cert_file).to eq('/tmp/foo/cert.pem')
end
end
end
describe '.ca_certs_paths' do describe '.ca_certs_paths' do
it 'returns all files specified by OpenSSL defaults' do it 'returns all files specified by OpenSSL defaults' do
cert_paths = Dir["#{OpenSSL::X509::DEFAULT_CERT_DIR}/*"] cert_paths = Dir["#{described_class.default_cert_dir}/*"]
expect(described_class.ca_certs_paths).to match_array(cert_paths + [sample_cert]) expect(described_class.ca_certs_paths).to match_array(cert_paths + [sample_cert])
end end

View File

@ -107,7 +107,7 @@ RSpec.describe Gitlab::X509::Signature do
f.print certificate.to_pem f.print certificate.to_pem
end end
stub_const("OpenSSL::X509::DEFAULT_CERT_FILE", file_path) allow(Gitlab::X509::Certificate).to receive(:default_cert_file).and_return(file_path)
allow(OpenSSL::X509::Store).to receive(:new).and_return(store) allow(OpenSSL::X509::Store).to receive(:new).and_return(store)
end end

View File

@ -788,20 +788,6 @@ RSpec.describe 'project routing' do
it 'to #test' do it 'to #test' do
expect(put('/gitlab/gitlabhq/-/settings/integrations/acme/test')).to route_to('projects/settings/integrations#test', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'acme') expect(put('/gitlab/gitlabhq/-/settings/integrations/acme/test')).to route_to('projects/settings/integrations#test', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'acme')
end end
context 'legacy routes' do
it 'to #edit' do
expect(get('/gitlab/gitlabhq/-/integrations/acme/edit')).to route_to('projects/settings/integrations#edit', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'acme')
end
it 'to #update' do
expect(put('/gitlab/gitlabhq/-/integrations/acme')).to route_to('projects/settings/integrations#update', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'acme')
end
it 'to #test' do
expect(put('/gitlab/gitlabhq/-/integrations/acme/test')).to route_to('projects/settings/integrations#test', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'acme')
end
end
end end
describe Projects::Settings::IntegrationHookLogsController do describe Projects::Settings::IntegrationHookLogsController do
@ -812,16 +798,6 @@ RSpec.describe 'project routing' do
it 'to #retry' do it 'to #retry' do
expect(post('/gitlab/gitlabhq/-/settings/integrations/acme/hook_logs/log/retry')).to route_to('projects/settings/integration_hook_logs#retry', namespace_id: 'gitlab', project_id: 'gitlabhq', integration_id: 'acme', id: 'log') expect(post('/gitlab/gitlabhq/-/settings/integrations/acme/hook_logs/log/retry')).to route_to('projects/settings/integration_hook_logs#retry', namespace_id: 'gitlab', project_id: 'gitlabhq', integration_id: 'acme', id: 'log')
end end
context 'legacy routes' do
it 'to #show' do
expect(get('/gitlab/gitlabhq/-/integrations/acme/hook_logs/log')).to route_to('projects/settings/integration_hook_logs#show', namespace_id: 'gitlab', project_id: 'gitlabhq', integration_id: 'acme', id: 'log')
end
it 'to #retry' do
expect(post('/gitlab/gitlabhq/-/integrations/acme/hook_logs/log/retry')).to route_to('projects/settings/integration_hook_logs#retry', namespace_id: 'gitlab', project_id: 'gitlabhq', integration_id: 'acme', id: 'log')
end
end
end end
describe Projects::TemplatesController, 'routing' do describe Projects::TemplatesController, 'routing' do