Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
421f6c92d5
commit
640007842a
|
@ -5,7 +5,7 @@ review-cleanup:
|
|||
extends:
|
||||
- .default-retry
|
||||
- .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
|
||||
environment:
|
||||
name: review/${CI_COMMIT_REF_SLUG}${FREQUENCY}
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
import { GlDropdown, GlDropdownItem, GlIcon } from '@gitlab/ui';
|
||||
import { mapActions, mapGetters, mapState } from 'vuex';
|
||||
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 DraftsCount from './drafts_count.vue';
|
||||
|
||||
|
@ -17,6 +18,7 @@ export default {
|
|||
computed: {
|
||||
...mapState('diffs', ['viewDiffsFileByFile']),
|
||||
...mapGetters('batchComments', ['draftsCount', 'sortedDrafts']),
|
||||
...mapGetters(['getNoteableData']),
|
||||
},
|
||||
methods: {
|
||||
...mapActions('diffs', ['setCurrentFileHash']),
|
||||
|
@ -24,12 +26,21 @@ export default {
|
|||
isLast(index) {
|
||||
return index === this.sortedDrafts.length - 1;
|
||||
},
|
||||
isOnLatestDiff(draft) {
|
||||
return draft.position?.head_sha === this.getNoteableData.diff_head_sha;
|
||||
},
|
||||
async onClickDraft(draft) {
|
||||
if (this.viewDiffsFileByFile && 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"
|
||||
@click="onClickDraft(draft)"
|
||||
>
|
||||
{{ isOnLatestDiff(draft) }}
|
||||
<preview-item :draft="draft" :is-last="isLast(index)" />
|
||||
</gl-dropdown-item>
|
||||
</gl-dropdown>
|
||||
|
|
|
@ -11,6 +11,8 @@ import { convertToCamelCase, convertToSnakeCase } from './text_utility';
|
|||
import { isObject } from './type_utility';
|
||||
import { getLocationHash } from './url_utility';
|
||||
|
||||
export const NO_SCROLL_TO_HASH_CLASS = 'js-no-scroll-to-hash';
|
||||
|
||||
export const getPagePath = (index = 0) => {
|
||||
const { page = '' } = document.body.dataset;
|
||||
return page.split(':')[index];
|
||||
|
@ -68,6 +70,10 @@ export const handleLocationHash = () => {
|
|||
hash = decodeURIComponent(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 fixedDiffStats = document.querySelector('.js-diff-files-changed');
|
||||
const fixedNav = document.querySelector('.navbar-gitlab');
|
||||
|
|
|
@ -1,33 +1,27 @@
|
|||
import createFlash from '~/flash';
|
||||
import { sanitize } from '~/lib/dompurify';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
import { historyPushState } from '~/lib/utils/common_utils';
|
||||
import { __ } from '~/locale';
|
||||
import { GlTabsBehavior, TAB_SHOWN_EVENT } from '~/tabs';
|
||||
import { GlTabsBehavior, TAB_SHOWN_EVENT, HISTORY_TYPE_HASH } from '~/tabs';
|
||||
|
||||
export default class Milestone {
|
||||
constructor() {
|
||||
this.tabsEl = document.querySelector('.js-milestone-tabs');
|
||||
this.glTabs = new GlTabsBehavior(this.tabsEl);
|
||||
this.loadedTabs = new WeakSet();
|
||||
|
||||
this.bindTabsSwitching();
|
||||
this.loadInitialTab();
|
||||
// eslint-disable-next-line no-new
|
||||
new GlTabsBehavior(this.tabsEl, { history: HISTORY_TYPE_HASH });
|
||||
}
|
||||
|
||||
bindTabsSwitching() {
|
||||
this.tabsEl.addEventListener(TAB_SHOWN_EVENT, (event) => {
|
||||
const tab = event.target;
|
||||
const { activeTabPanel } = event.detail;
|
||||
historyPushState(tab.getAttribute('href'));
|
||||
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) {
|
||||
const { endpoint } = tab.dataset;
|
||||
|
||||
|
|
|
@ -14,3 +14,6 @@ export const ATTR_ROLE = 'role';
|
|||
export const ATTR_TABINDEX = 'tabindex';
|
||||
|
||||
export const TAB_SHOWN_EVENT = 'gl-tab-shown';
|
||||
|
||||
export const HISTORY_TYPE_HASH = 'hash';
|
||||
export const ALLOWED_HISTORY_TYPES = [HISTORY_TYPE_HASH];
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { uniqueId } from 'lodash';
|
||||
import { historyReplaceState, NO_SCROLL_TO_HASH_CLASS } from '~/lib/utils/common_utils';
|
||||
import {
|
||||
ACTIVE_TAB_CLASSES,
|
||||
ATTR_ROLE,
|
||||
|
@ -12,9 +13,11 @@ import {
|
|||
KEY_CODE_RIGHT,
|
||||
KEY_CODE_DOWN,
|
||||
TAB_SHOWN_EVENT,
|
||||
HISTORY_TYPE_HASH,
|
||||
ALLOWED_HISTORY_TYPES,
|
||||
} 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
|
||||
|
@ -88,9 +91,13 @@ export class GlTabsBehavior {
|
|||
/**
|
||||
* 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) {
|
||||
throw new Error('Cannot instantiate GlTabsBehavior without an element');
|
||||
}
|
||||
|
@ -100,8 +107,11 @@ export class GlTabsBehavior {
|
|||
this.tabs = this.getTabs();
|
||||
this.activeTab = null;
|
||||
|
||||
this.history = ALLOWED_HISTORY_TYPES.includes(history) ? history : null;
|
||||
|
||||
this.setAccessibilityAttrs();
|
||||
this.bindEvents();
|
||||
if (this.history === HISTORY_TYPE_HASH) this.loadInitialTab();
|
||||
}
|
||||
|
||||
setAccessibilityAttrs() {
|
||||
|
@ -128,6 +138,7 @@ export class GlTabsBehavior {
|
|||
tab.setAttribute(ATTR_ARIA_CONTROLS, tabPanel.id);
|
||||
}
|
||||
|
||||
tabPanel.classList.add(NO_SCROLL_TO_HASH_CLASS);
|
||||
tabPanel.setAttribute(ATTR_ROLE, 'tabpanel');
|
||||
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() {
|
||||
const currentTabIndex = this.tabs.indexOf(this.activeTab);
|
||||
|
||||
|
@ -216,6 +232,7 @@ export class GlTabsBehavior {
|
|||
const tabPanel = this.getPanelForTab(tabToActivate);
|
||||
tabPanel.classList.add(ACTIVE_PANEL_CLASS);
|
||||
|
||||
if (this.history === HISTORY_TYPE_HASH) historyReplaceState(tabToActivate.getAttribute('href'));
|
||||
this.activeTab = tabToActivate;
|
||||
|
||||
this.dispatchTabShown(tabToActivate, tabPanel);
|
||||
|
|
|
@ -248,7 +248,7 @@ export default {
|
|||
labels: this.enableAutocomplete,
|
||||
snippets: this.enableAutocomplete,
|
||||
vulnerabilities: this.enableAutocomplete,
|
||||
contacts: this.enableAutocomplete && this.glFeatures.contactsAutocomplete,
|
||||
contacts: this.enableAutocomplete,
|
||||
},
|
||||
true,
|
||||
);
|
||||
|
|
|
@ -41,7 +41,6 @@ class Projects::IssuesController < Projects::ApplicationController
|
|||
before_action :authorize_download_code!, only: [:related_branches]
|
||||
|
||||
before_action do
|
||||
push_frontend_feature_flag(:contacts_autocomplete, project&.group)
|
||||
push_frontend_feature_flag(:incident_timeline, project)
|
||||
end
|
||||
|
||||
|
|
|
@ -15,7 +15,7 @@ module UsersHelper
|
|||
end
|
||||
|
||||
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
|
||||
|
||||
|
|
|
@ -47,7 +47,7 @@ module BulkImports
|
|||
end
|
||||
|
||||
def non_patch_source_version
|
||||
Gitlab::VersionInfo.new(source_version.major, source_version.minor, 0)
|
||||
source_version.without_patch
|
||||
end
|
||||
|
||||
def log_skipped_pipeline(pipeline, minimum_version, maximum_version)
|
||||
|
|
|
@ -13,6 +13,7 @@ module Issues
|
|||
# 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.
|
||||
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)
|
||||
@spam_params = spam_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)
|
||||
resolve_discussions_with_issue(issue)
|
||||
create_escalation_status(issue)
|
||||
try_to_associate_contact(issue)
|
||||
try_to_associate_contacts(issue)
|
||||
|
||||
super
|
||||
end
|
||||
|
@ -85,7 +86,7 @@ module Issues
|
|||
|
||||
private
|
||||
|
||||
attr_reader :spam_params
|
||||
attr_reader :spam_params, :extra_params
|
||||
|
||||
def create_escalation_status(issue)
|
||||
::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
|
||||
end
|
||||
|
||||
def try_to_associate_contact(issue)
|
||||
def try_to_associate_contacts(issue)
|
||||
return unless issue.external_author
|
||||
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
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
= 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' }
|
||||
= form_errors(@group)
|
||||
= form_errors(@group, pajamas_alert: true)
|
||||
|
||||
%fieldset
|
||||
.row
|
||||
|
|
|
@ -1,10 +1,7 @@
|
|||
- to_names = content_tag(:strong, issuable.assignees.any? ? sanitize_name(issuable.assignee_list) : s_('Unassigned'))
|
||||
|
||||
%p
|
||||
Assignee changed
|
||||
- if previous_assignees.any?
|
||||
from
|
||||
%strong= sanitize_name(previous_assignees.map(&:name).to_sentence)
|
||||
to
|
||||
- if issuable.assignees.any?
|
||||
%strong= sanitize_name(issuable.assignee_list)
|
||||
= 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 })
|
||||
- else
|
||||
%strong Unassigned
|
||||
= html_escape(s_('Notify|Assignee changed to %{toNames}').html_safe % { toNames: to_names})
|
||||
|
|
|
@ -1,3 +1,2 @@
|
|||
%p
|
||||
#{'Label'.pluralize(@label_names.size)} added:
|
||||
%em= @label_names.to_sentence
|
||||
= html_escape(n_('Label added: %{labels}', 'Labels added: %{labels}', @label_names.size).html_safe % { labels: content_tag(:em, @label_names.to_sentence).html_safe })
|
||||
|
|
|
@ -22,12 +22,12 @@
|
|||
{ include_blank: s_("Profiles|Do not show on profile") },
|
||||
{ class: 'gl-form-select custom-select', disabled: email_change_disabled }
|
||||
%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
|
||||
- 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_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')
|
||||
.gl-md-form-input-lg
|
||||
= form.select :commit_email,
|
||||
|
|
|
@ -2,8 +2,8 @@
|
|||
- if user.read_only_attribute?(:name)
|
||||
= form.text_field :name, class: 'gl-form-input form-control', required: true, readonly: true
|
||||
%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
|
||||
= 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
|
||||
= s_("Profiles|Enter your name, so people you know can recognize you")
|
||||
= s_("Profiles|Enter your name, so people you know can recognize you.")
|
||||
|
|
|
@ -74,7 +74,7 @@
|
|||
.form-group.gl-form-group
|
||||
= status_form.gitlab_ui_checkbox_component :availability,
|
||||
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" } },
|
||||
checked_value: availability["busy"],
|
||||
unchecked_value: availability["not_set"]
|
||||
|
@ -83,7 +83,7 @@
|
|||
.row.user-time-preferences.js-search-settings-section
|
||||
.col-lg-4.profile-settings-sidebar
|
||||
%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
|
||||
%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 } } )
|
||||
|
@ -95,7 +95,7 @@
|
|||
%h4.gl-mt-0
|
||||
= s_("Profiles|Main settings")
|
||||
%p
|
||||
= s_("Profiles|This information will appear on your profile")
|
||||
= s_("Profiles|This information will appear on your profile.")
|
||||
- if current_user.ldap_user?
|
||||
= s_("Profiles|Some options are unavailable for LDAP accounts")
|
||||
.col-lg-8
|
||||
|
@ -109,12 +109,12 @@
|
|||
= f.label :pronouns, s_('Profiles|Pronouns')
|
||||
= f.text_field :pronouns, class: 'gl-form-input form-control gl-md-form-input-lg'
|
||||
%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
|
||||
= f.label :pronunciation, s_('Profiles|Pronunciation')
|
||||
= f.text_field :pronunciation, class: 'gl-form-input form-control gl-md-form-input-lg'
|
||||
%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/email_settings', form: f
|
||||
.form-group.gl-form-group
|
||||
|
@ -146,17 +146,17 @@
|
|||
= f.label :organization, s_('Profiles|Organization')
|
||||
= f.text_field :organization, class: 'gl-form-input form-control gl-md-form-input-lg'
|
||||
%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
|
||||
= f.label :bio, s_('Profiles|Bio')
|
||||
= f.text_area :bio, class: 'gl-form-input gl-form-textarea form-control', rows: 4, maxlength: 250
|
||||
%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
|
||||
%fieldset.form-group.gl-form-group
|
||||
%legend.col-form-label.col-form-label
|
||||
= _('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')
|
||||
= 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
|
||||
|
@ -164,7 +164,7 @@
|
|||
= s_("Profiles|Private contributions")
|
||||
= f.gitlab_ui_checkbox_component :include_private_contributions,
|
||||
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
|
||||
= 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'
|
||||
|
|
|
@ -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
|
|
@ -221,20 +221,6 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
|
|||
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
|
||||
collection do
|
||||
get :recent
|
||||
|
|
|
@ -66,7 +66,8 @@ Supported attributes:
|
|||
| `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 |
|
||||
|:-------------------------|:---------|:----------------------|
|
||||
|
@ -151,6 +152,14 @@ For information about writing attribute descriptions, see the [GraphQL API descr
|
|||
|
||||
## 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
|
||||
always be in code blocks using backticks (`` ` ``).
|
||||
|
||||
|
|
|
@ -118,6 +118,9 @@ organizations using the GraphQL API.
|
|||
|
||||
## 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
|
||||
|
||||
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.
|
||||
> - [Enabled on GitLab.com and self-managed](https://gitlab.com/gitlab-org/gitlab/-/issues/352123) in GitLab 15.0.
|
||||
|
||||
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.
|
||||
> - [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.
|
||||
|
||||
When you use the `/add_contacts` or `/remove_contacts` quick actions, follow them with `[contact:` and an autocomplete list appears:
|
||||
|
||||
|
|
|
@ -96,8 +96,8 @@ module Backup
|
|||
|
||||
def build_env
|
||||
{
|
||||
'SSL_CERT_FILE' => OpenSSL::X509::DEFAULT_CERT_FILE,
|
||||
'SSL_CERT_DIR' => OpenSSL::X509::DEFAULT_CERT_DIR
|
||||
'SSL_CERT_FILE' => Gitlab::X509::Certificate.default_cert_file,
|
||||
'SSL_CERT_DIR' => Gitlab::X509::Certificate.default_cert_dir
|
||||
}.merge(ENV)
|
||||
end
|
||||
|
||||
|
|
|
@ -25,18 +25,16 @@ module Gitlab
|
|||
|
||||
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
|
||||
.reject { |release| release.major > @gitlab_version.major }
|
||||
.reject do |release|
|
||||
release_minor_version = version_without_patch(release)
|
||||
.reject { |release| release.major > @gitlab_version.major }
|
||||
.reject do |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
|
||||
next false if version_without_patch(runner_version) == release_minor_version
|
||||
|
||||
release_minor_version > gitlab_minor_version
|
||||
end
|
||||
release.without_patch > gitlab_minor_version
|
||||
end
|
||||
|
||||
return :recommended if available_releases.any? { |available_rel| patch_update?(available_rel, runner_version) }
|
||||
return :recommended if outside_backport_window?(runner_version, releases)
|
||||
|
@ -63,19 +61,15 @@ module Gitlab
|
|||
def outside_backport_window?(runner_version, releases)
|
||||
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
|
||||
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
|
||||
|
||||
# https://docs.gitlab.com/ee/policy/maintenance.html#backporting-to-older-releases
|
||||
latest_version_position - runner_version_position > 2
|
||||
end
|
||||
|
||||
def version_without_patch(version)
|
||||
::Gitlab::VersionInfo.new(version.major, version.minor, 0)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -98,7 +98,10 @@ module Gitlab
|
|||
title: mail.subject,
|
||||
description: message_including_template,
|
||||
confidential: true,
|
||||
external_author: from_address
|
||||
external_author: from_address,
|
||||
extra_params: {
|
||||
cc: mail.cc
|
||||
}
|
||||
},
|
||||
spam_params: nil
|
||||
).execute
|
||||
|
|
|
@ -52,5 +52,21 @@ module Gitlab
|
|||
def valid?
|
||||
@major >= 0 && @minor >= 0 && @patch >= 0 && @major + @minor + @patch > 0
|
||||
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
|
||||
|
|
|
@ -23,6 +23,18 @@ module Gitlab
|
|||
include ::Gitlab::Utils::StrongMemoize
|
||||
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)
|
||||
key = OpenSSL::PKey::RSA.new(key_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
|
||||
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)
|
||||
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
|
||||
end
|
||||
|
||||
|
@ -61,6 +73,11 @@ module Gitlab
|
|||
clear_memoization(:ca_certs_bundle)
|
||||
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
|
||||
#
|
||||
# Ruby OpenSSL::X509::Certificate.new will only load the first
|
||||
|
|
|
@ -59,7 +59,7 @@ module Gitlab
|
|||
|
||||
if Feature.enabled?(:x509_forced_cert_loading, type: :ops)
|
||||
# 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
|
||||
|
||||
# valid_signing_time? checks the time attributes already
|
||||
|
|
|
@ -22411,6 +22411,11 @@ msgstr ""
|
|||
msgid "Label actions dropdown"
|
||||
msgstr ""
|
||||
|
||||
msgid "Label added: %{labels}"
|
||||
msgid_plural "Labels added: %{labels}"
|
||||
msgstr[0] ""
|
||||
msgstr[1] ""
|
||||
|
||||
msgid "Label priority"
|
||||
msgstr ""
|
||||
|
||||
|
@ -26188,6 +26193,12 @@ msgstr ""
|
|||
msgid "Notify|%{author_link}'s issue %{issue_reference_link} is due soon."
|
||||
msgstr ""
|
||||
|
||||
msgid "Notify|Assignee changed from %{fromNames} to %{toNames}"
|
||||
msgstr ""
|
||||
|
||||
msgid "Notify|Assignee changed to %{toNames}"
|
||||
msgstr ""
|
||||
|
||||
msgid "Notify|Author: %{author_name}"
|
||||
msgstr ""
|
||||
|
||||
|
@ -29204,7 +29215,7 @@ msgstr ""
|
|||
msgid "Profiles|An error occurred while updating your username, please try again."
|
||||
msgstr ""
|
||||
|
||||
msgid "Profiles|An indicator appears next to your name and avatar"
|
||||
msgid "Profiles|An indicator appears next to your name and avatar."
|
||||
msgstr ""
|
||||
|
||||
msgid "Profiles|Avatar cropper"
|
||||
|
@ -29231,7 +29242,7 @@ msgstr ""
|
|||
msgid "Profiles|Choose file..."
|
||||
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 ""
|
||||
|
||||
msgid "Profiles|City, country"
|
||||
|
@ -29273,7 +29284,7 @@ msgstr ""
|
|||
msgid "Profiles|Do not show on profile"
|
||||
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 ""
|
||||
|
||||
msgid "Profiles|Edit Profile"
|
||||
|
@ -29282,16 +29293,16 @@ msgstr ""
|
|||
msgid "Profiles|Ensure you have two-factor authentication recovery codes stored in a safe place."
|
||||
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 ""
|
||||
|
||||
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 ""
|
||||
|
||||
msgid "Profiles|Enter your password to confirm the email change"
|
||||
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 ""
|
||||
|
||||
msgid "Profiles|Example: MacBook key"
|
||||
|
@ -29414,7 +29425,7 @@ msgstr ""
|
|||
msgid "Profiles|Set new profile picture"
|
||||
msgstr ""
|
||||
|
||||
msgid "Profiles|Set your local time zone"
|
||||
msgid "Profiles|Set your local time zone."
|
||||
msgstr ""
|
||||
|
||||
msgid "Profiles|Social sign-in"
|
||||
|
@ -29426,7 +29437,7 @@ msgstr ""
|
|||
msgid "Profiles|Static object token was successfully reset"
|
||||
msgstr ""
|
||||
|
||||
msgid "Profiles|Tell us about yourself in fewer than 250 characters"
|
||||
msgid "Profiles|Tell us about yourself in fewer than 250 characters."
|
||||
msgstr ""
|
||||
|
||||
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."
|
||||
msgstr ""
|
||||
|
||||
msgid "Profiles|This email will be displayed on your public profile"
|
||||
msgid "Profiles|This email will be displayed on your public profile."
|
||||
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 ""
|
||||
|
||||
msgid "Profiles|This emoji and message will appear on your profile and throughout the interface."
|
||||
msgstr ""
|
||||
|
||||
msgid "Profiles|This information will appear on your profile"
|
||||
msgid "Profiles|This information will appear on your profile."
|
||||
msgstr ""
|
||||
|
||||
msgid "Profiles|Time settings"
|
||||
|
@ -29489,7 +29500,7 @@ msgstr ""
|
|||
msgid "Profiles|What's your status?"
|
||||
msgstr ""
|
||||
|
||||
msgid "Profiles|Who you represent or work for"
|
||||
msgid "Profiles|Who you represent or work for."
|
||||
msgstr ""
|
||||
|
||||
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"
|
||||
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"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -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
|
||||
From: Jake the Dog <jake@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>
|
||||
Subject: The message subject! @all
|
||||
Mime-Version: 1.0
|
||||
|
|
|
@ -1,8 +1,15 @@
|
|||
import Vue, { nextTick } from 'vue';
|
||||
import Vuex from 'vuex';
|
||||
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';
|
||||
|
||||
jest.mock('~/lib/utils/url_utility', () => ({
|
||||
visitUrl: jest.fn(),
|
||||
setUrlParams: jest.requireActual('~/lib/utils/url_utility').setUrlParams,
|
||||
}));
|
||||
|
||||
Vue.use(Vuex);
|
||||
|
||||
let wrapper;
|
||||
|
@ -27,6 +34,11 @@ function factory({ viewDiffsFileByFile = false, draftsCount = 1, sortedDrafts =
|
|||
actions: { scrollToDraft },
|
||||
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 });
|
||||
});
|
||||
|
||||
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`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -88,6 +88,28 @@ describe('common_utils', () => {
|
|||
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', () => {
|
||||
document.body.innerHTML += `
|
||||
<div id="parent">
|
||||
|
|
|
@ -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 { 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 setWindowLocation from 'helpers/set_window_location_helper';
|
||||
|
||||
const tabsFixture = getFixture('tabs/tabs.html');
|
||||
|
||||
global.CSS = {
|
||||
escape: (val) => val,
|
||||
};
|
||||
|
||||
describe('GlTabsBehavior', () => {
|
||||
let glTabs;
|
||||
let tabShownEventSpy;
|
||||
|
@ -41,6 +48,7 @@ describe('GlTabsBehavior', () => {
|
|||
});
|
||||
|
||||
expect(panel.classList.contains(ACTIVE_PANEL_CLASS)).toBe(true);
|
||||
expect(panel.classList.contains(NO_SCROLL_TO_HASH_CLASS)).toBe(true);
|
||||
};
|
||||
|
||||
const expectInactiveTabAndPanel = (name) => {
|
||||
|
@ -67,6 +75,7 @@ describe('GlTabsBehavior', () => {
|
|||
});
|
||||
|
||||
expect(panel.classList.contains(ACTIVE_PANEL_CLASS)).toBe(false);
|
||||
expect(panel.classList.contains(NO_SCROLL_TO_HASH_CLASS)).toBe(true);
|
||||
};
|
||||
|
||||
const expectGlTabShownEvent = (name) => {
|
||||
|
@ -263,4 +272,98 @@ describe('GlTabsBehavior', () => {
|
|||
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);
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -67,11 +67,6 @@ describe('Markdown field component', () => {
|
|||
enablePreview,
|
||||
restrictedToolBarItems,
|
||||
},
|
||||
provide: {
|
||||
glFeatures: {
|
||||
contactsAutocomplete: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
|
|
@ -16,8 +16,8 @@ RSpec.describe Backup::GitalyBackup do
|
|||
|
||||
let(:expected_env) do
|
||||
{
|
||||
'SSL_CERT_FILE' => OpenSSL::X509::DEFAULT_CERT_FILE,
|
||||
'SSL_CERT_DIR' => OpenSSL::X509::DEFAULT_CERT_DIR
|
||||
'SSL_CERT_FILE' => Gitlab::X509::Certificate.default_cert_file,
|
||||
'SSL_CERT_DIR' => Gitlab::X509::Certificate.default_cert_dir
|
||||
}.merge(ENV)
|
||||
end
|
||||
|
||||
|
|
|
@ -52,14 +52,6 @@ RSpec.describe Gitlab::Email::Handler::ServiceDeskHandler do
|
|||
expect(new_issue.issue_email_participants.first.email).to eq(author_email)
|
||||
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
|
||||
expect { receiver.execute }.to have_enqueued_job.on_queue('mailers')
|
||||
end
|
||||
|
@ -77,6 +69,16 @@ RSpec.describe Gitlab::Email::Handler::ServiceDeskHandler do
|
|||
context 'when everything is fine' do
|
||||
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
|
||||
let(:email_raw) { fixture_file('emails/service_desk_legacy.eml') }
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
require 'fast_spec_helper'
|
||||
|
||||
RSpec.describe 'Gitlab::VersionInfo' do
|
||||
before do
|
||||
|
@ -13,7 +13,7 @@ RSpec.describe 'Gitlab::VersionInfo' do
|
|||
@v2_0_0 = Gitlab::VersionInfo.new(2, 0, 0)
|
||||
end
|
||||
|
||||
context '>' do
|
||||
describe '>' do
|
||||
it { expect(@v2_0_0).to be > @v1_1_0 }
|
||||
it { expect(@v1_1_0).to be > @v1_0_1 }
|
||||
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 }
|
||||
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 >= @v1_1_0 }
|
||||
end
|
||||
|
||||
context '<' do
|
||||
describe '<' do
|
||||
it { expect(@v0_0_1).to be < @v0_1_0 }
|
||||
it { expect(@v0_1_0).to be < @v1_0_0 }
|
||||
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 }
|
||||
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 <= @v0_1_0 }
|
||||
end
|
||||
|
||||
context '==' do
|
||||
describe '==' do
|
||||
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(@v1_0_0).to eq(Gitlab::VersionInfo.new(1, 0, 0)) }
|
||||
end
|
||||
|
||||
context '!=' do
|
||||
describe '!=' do
|
||||
it { expect(@v0_0_1).not_to eq(@v0_1_0) }
|
||||
end
|
||||
|
||||
context 'unknown' do
|
||||
describe '.unknown' do
|
||||
it { expect(@unknown).not_to be @v0_0_1 }
|
||||
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) }
|
||||
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.1")).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 }
|
||||
end
|
||||
|
||||
context 'to_s' do
|
||||
describe '.to_s' do
|
||||
it { expect(@v1_0_0.to_s).to eq("1.0.0") }
|
||||
it { expect(@unknown.to_s).to eq("Unknown") }
|
||||
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
|
||||
|
|
|
@ -116,9 +116,69 @@ RSpec.describe Gitlab::X509::Certificate do
|
|||
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
|
||||
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])
|
||||
end
|
||||
|
|
|
@ -107,7 +107,7 @@ RSpec.describe Gitlab::X509::Signature do
|
|||
f.print certificate.to_pem
|
||||
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)
|
||||
end
|
||||
|
|
|
@ -788,20 +788,6 @@ RSpec.describe 'project routing' 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')
|
||||
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
|
||||
|
||||
describe Projects::Settings::IntegrationHookLogsController do
|
||||
|
@ -812,16 +798,6 @@ RSpec.describe 'project routing' 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')
|
||||
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
|
||||
|
||||
describe Projects::TemplatesController, 'routing' do
|
||||
|
|
Loading…
Reference in New Issue