Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2022-03-03 18:19:46 +00:00
parent 42afc4d656
commit 1e28c9498f
38 changed files with 311 additions and 80 deletions

View File

@ -20,6 +20,8 @@ tasks:
gdk config set gitlab.rails.port 443
gdk config set gitlab.rails.https.enabled true
gdk config set webpack.host 127.0.0.1
gdk config set webpack.static false
gdk config set webpack.live_reload false
# make documentation builds available
gdk config set gitlab_docs.enabled true
# reconfigure GDK
@ -49,6 +51,8 @@ tasks:
gdk config set gitlab.rails.port 443
gdk config set gitlab.rails.https.enabled true
gdk config set webpack.host 127.0.0.1
gdk config set webpack.static false
gdk config set webpack.live_reload false
# reconfigure GDK
echo "$(date) Reconfiguring GDK" | tee -a /workspace/startup.log
gdk reconfigure

View File

@ -88,7 +88,7 @@ function renderMermaidEl(el, source) {
const iframeEl = document.createElement('iframe');
setAttributes(iframeEl, {
src: getSandboxFrameSrc(),
sandbox: 'allow-scripts',
sandbox: 'allow-scripts allow-popups',
frameBorder: 0,
scrolling: 'no',
width: '100%',

View File

@ -1,3 +1,4 @@
import { loadingIconForLegacyJS } from '~/loading_icon_for_legacy_js';
import { FILTER_TYPE } from './constants';
import DropdownUtils from './dropdown_utils';
import FilteredSearchDropdownManager from './filtered_search_dropdown_manager';
@ -13,7 +14,7 @@ export default class FilteredSearchDropdown {
this.filter = filter;
this.dropdown = dropdown;
this.loadingTemplate = `<div class="filter-dropdown-loading">
<span class="spinner"></span>
${loadingIconForLegacyJS().outerHTML}
</div>`;
this.bindEvents();
}

View File

@ -128,7 +128,7 @@ export default {
});
},
newIssueTypeText() {
return sprintf(__('New %{issueType}'), { issueType: this.issueType });
return sprintf(__('New related %{issueType}'), { issueType: this.issueType });
},
showToggleIssueStateButton() {
const canClose = !this.isClosed && this.canUpdateIssue;

View File

@ -1,3 +1,4 @@
/* globals LIVE_RELOAD */
const div = document.createElement('div');
Object.assign(div.style, {
@ -15,6 +16,10 @@ Object.assign(div.style, {
'text-align': 'center',
});
const reloadMessage = LIVE_RELOAD
? 'You have live_reload enabled, the page will reload automatically when complete.'
: 'You have live_reload disabled, the page will reload automatically in a few seconds.';
div.innerHTML = `
<!-- https://github.com/webpack/media/blob/master/logo/icon-square-big.svg -->
<svg height="50" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1200 1200">
@ -30,9 +35,15 @@ div.innerHTML = `
Learn more <a href="https://gitlab.com/gitlab-org/gitlab-development-kit/-/blob/main/doc/configuration.md#webpack-settings">here</a>.
</p>
<p>
If you have live_reload enabled, the page will reload automatically when complete.<br />
Otherwise, please <a href="">reload the page manually in a few seconds</a>
${reloadMessage}<br />
If it doesn't, please <a href="">reload the page manually</a>.
</p>
`;
document.body.append(div);
if (!LIVE_RELOAD) {
setTimeout(() => {
window.location.reload();
}, 5000);
}

View File

@ -993,19 +993,6 @@ input {
.top-nav-toggle .dropdown-icon {
margin-right: 0.5rem;
}
.tanuki-logo .tanuki-left-ear,
.tanuki-logo .tanuki-right-ear,
.tanuki-logo .tanuki-nose {
fill: #e24329;
}
.tanuki-logo .tanuki-left-eye,
.tanuki-logo .tanuki-right-eye {
fill: #fc6d26;
}
.tanuki-logo .tanuki-left-cheek,
.tanuki-logo .tanuki-right-cheek {
fill: #fca326;
}
.context-header {
position: relative;
margin-right: 2px;

View File

@ -978,19 +978,6 @@ input {
.top-nav-toggle .dropdown-icon {
margin-right: 0.5rem;
}
.tanuki-logo .tanuki-left-ear,
.tanuki-logo .tanuki-right-ear,
.tanuki-logo .tanuki-nose {
fill: #e24329;
}
.tanuki-logo .tanuki-left-eye,
.tanuki-logo .tanuki-right-eye {
fill: #fc6d26;
}
.tanuki-logo .tanuki-left-cheek,
.tanuki-logo .tanuki-right-cheek {
fill: #fca326;
}
.context-header {
position: relative;
margin-right: 2px;

View File

@ -514,19 +514,6 @@ label.label-bold {
.navbar-empty .brand-header-logo {
max-height: 100%;
}
.tanuki-logo .tanuki-left-ear,
.tanuki-logo .tanuki-right-ear,
.tanuki-logo .tanuki-nose {
fill: #e24329;
}
.tanuki-logo .tanuki-left-eye,
.tanuki-logo .tanuki-right-eye {
fill: #fc6d26;
}
.tanuki-logo .tanuki-left-cheek,
.tanuki-logo .tanuki-right-cheek {
fill: #fca326;
}
input::-moz-placeholder {
color: #868686;
opacity: 1;

View File

@ -106,6 +106,8 @@ class Projects::IssuesController < Projects::ApplicationController
@issue = @noteable = service.execute
@add_related_issue = add_related_issue
@merge_request_to_resolve_discussions_of = service.merge_request_to_resolve_discussions_of
if params[:discussion_to_resolve]
@ -122,6 +124,7 @@ class Projects::IssuesController < Projects::ApplicationController
def create
create_params = issue_params.merge(
add_related_issue: add_related_issue,
merge_request_to_resolve_discussions_of: params[:merge_request_to_resolve_discussions_of],
discussion_to_resolve: params[:discussion_to_resolve]
)
@ -377,6 +380,11 @@ class Projects::IssuesController < Projects::ApplicationController
action_name == 'service_desk'
end
def add_related_issue
add_related_issue = project.issues.find_by_iid(params[:add_related_issue])
add_related_issue if Ability.allowed?(current_user, :read_issue, add_related_issue)
end
# Overridden in EE
def create_vulnerability_issue_feedback(issue); end
end

View File

@ -38,6 +38,8 @@ module AppearancesHelper
def brand_header_logo
if current_appearance&.header_logo?
image_tag current_appearance.header_logo_path, class: 'brand-header-logo'
elsif Feature.enabled?(:ukraine_support_tanuki)
render partial: 'shared/logo_ukraine', formats: :svg
else
render partial: 'shared/logo', formats: :svg
end

View File

@ -169,7 +169,7 @@ module IssuesHelper
end
def issue_header_actions_data(project, issuable, current_user)
new_issuable_params = { issue: { description: _('Related to #%{issue_id}.') % { issue_id: issuable.iid } + "\n\n" } }
new_issuable_params = { issue: {}, add_related_issue: issuable.iid }
if issuable.incident?
new_issuable_params[:issuable_template] = 'incident'
new_issuable_params[:issue][:issue_type] = 'incident'

View File

@ -135,6 +135,7 @@ class User < ApplicationRecord
has_many :u2f_registrations, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :webauthn_registrations
has_many :chat_names, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :saved_replies, class_name: '::Users::SavedReply'
has_one :user_synced_attributes_metadata, autosave: true
has_one :aws_role, class_name: 'Aws::Role'

View File

@ -0,0 +1,19 @@
# frozen_string_literal: true
module Users
class SavedReply < ApplicationRecord
self.table_name = 'saved_replies'
belongs_to :user
validates :user_id, :name, :content, presence: true
validates :name,
length: { maximum: 255 },
uniqueness: { scope: [:user_id] },
format: {
with: Gitlab::Regex.saved_reply_name_regex,
message: Gitlab::Regex.saved_reply_name_regex_message
}
validates :content, length: { maximum: 10000 }
end
end

View File

@ -23,6 +23,7 @@ module Issues
handle_move_between_ids(@issue)
@add_related_issue ||= params.delete(:add_related_issue)
filter_resolve_discussion_params
create(@issue, skip_system_notes: skip_system_notes)
@ -52,6 +53,7 @@ module Issues
# Add new items to Issues::AfterCreateService if they can be performed in Sidekiq
def after_create(issue)
user_agent_detail_service.create
handle_add_related_issue(issue)
resolve_discussions_with_issue(issue)
create_escalation_status(issue)
@ -91,6 +93,12 @@ module Issues
def user_agent_detail_service
UserAgentDetailService.new(spammable: @issue, spam_params: spam_params)
end
def handle_add_related_issue(issue)
return unless @add_related_issue
IssueLinks::CreateService.new(issue, issue.author, { target_issuable: @add_related_issue }).execute
end
end
end

View File

@ -0,0 +1,5 @@
<svg width="24" height="24" class="tanuki-logo" viewBox="0 0 24 24">
<path d="M4.89929534,0.3165 L7.56629534,8.5025 L16.3922953,8.5025 L19.0592953,0.3165 C19.1962953,-0.1055 19.8432953,-0.1055 19.9792953,0.3165 L23.9122953,12.6095 C23.9722953,12.7935 23.9722953,12.9895 23.9192953,13.1695 L0.0392953418,13.1695 C-0.0143874393,12.9863283 -0.0119492421,12.7912726 0.0462953418,12.6095 L3.97929534,0.3165 C4.11529534,-0.1055 4.76229534,-0.1055 4.89929534,0.3165 Z" id="Path" fill="#005BBB"></path>
<path d="M7.20329534,9.0025 L16.7552953,9.0025 L16.8682953,8.6575 L19.5182953,0.5185 L23.4362953,12.7615 C23.4961172,12.9376949 23.435535,13.1323657 23.2862953,13.2435 L23.2852953,13.2455 L11.9852953,21.4655 L11.9792953,21.4715 L0.673295342,13.2455 C0.522422013,13.1321007 0.462258936,12.9374792 0.522295342,12.7615 L4.43929534,0.5185 L7.09029534,8.6585 L7.20329534,9.0025 Z" id="Shape" stroke="#FFFFFF" opacity="0.32" stroke-linejoin="round"></path>
<path d="M0.0012953418,12.8575 C-0.0152229638,13.1685309 0.127095079,13.4667211 0.379295342,13.6495 L11.9792953,22.0895 L11.9862953,22.0845 L11.9922953,22.0895 L11.9872953,22.0835 L23.5792953,13.6495 C23.8319507,13.466647 23.9743476,13.1679148 23.9572953,12.8565 L0.0012953418,12.8565 L0.0012953418,12.8575 Z" id="Path" fill="#FFD500"></path>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -4,6 +4,16 @@
- has_due_date = issuable.has_attribute?(:due_date)
- form = local_assigns.fetch(:form)
- if @add_related_issue
.form-group.row
.offset-sm-2.col-sm-10
.form-check
= check_box_tag :add_related_issue, @add_related_issue.iid, true, class: 'form-check-input'
= label_tag :add_related_issue, class: 'form-check-label' do
- add_related_issue_link = link_to "\##{@add_related_issue.iid}", issue_path(@add_related_issue), class: ['has-tooltip'], title: @add_related_issue.title
#{_('Relate to %{issuable_type} %{add_related_issue_link}').html_safe % { issuable_type: @add_related_issue.issue_type, add_related_issue_link: add_related_issue_link }}
%p.text-muted= _('Adds this %{issuable_type} as related to the %{issuable_type} it was created from') % { issuable_type: @add_related_issue.issue_type }
- if issuable.respond_to?(:confidential) && can?(current_user, :set_confidentiality, issuable)
.form-group.row
.offset-sm-2.col-sm-10

View File

@ -0,0 +1,8 @@
---
name: ukraine_support_tanuki
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/82050
rollout_issue_url:
milestone: '14.9'
type: development
group: group::editor
default_enabled: false

View File

@ -102,7 +102,7 @@ class IncrementalWebpackCompiler {
setTimeout(() => {
devServer.invalidate(() => {
if (devServer.sockets) {
if (Array.isArray(devServer.webSocketServer && devServer.webSocketServer.clients)) {
devServer.sendMessage(devServer.webSocketServer.clients, 'static-changed');
}
});

View File

@ -677,6 +677,7 @@ module.exports = {
IS_JH: IS_JH ? 'window.gon && window.gon.jh' : JSON.stringify(false),
// This is used by Sourcegraph because these assets are loaded dnamically
'process.env.SOURCEGRAPH_PUBLIC_PATH': JSON.stringify(SOURCEGRAPH_PUBLIC_PATH),
...(IS_PRODUCTION ? {} : { LIVE_RELOAD: DEV_SERVER_LIVERELOAD }),
}),
/* Pikaday has a optional dependency to moment.

View File

@ -0,0 +1,20 @@
# frozen_string_literal: true
class CreateSavedReplies < Gitlab::Database::Migration[1.0]
enable_lock_retries!
def up
create_table :saved_replies do |t|
t.references :user, index: false, null: false, foreign_key: { on_delete: :cascade }
t.timestamps_with_timezone null: false
t.text :name, null: false, limit: 255
t.text :content, null: false, limit: 10000
t.index [:user_id, :name], name: 'index_saved_replies_on_name_text_pattern_ops', unique: true, opclass: { name: :text_pattern_ops }
end
end
def down
drop_table :saved_replies, if_exists: true
end
end

View File

@ -0,0 +1 @@
5931c4981c89d65c5aaca05dc8375c2c21bb595e28354d6623986d906ece165d

View File

@ -20171,6 +20171,26 @@ CREATE SEQUENCE saml_providers_id_seq
ALTER SEQUENCE saml_providers_id_seq OWNED BY saml_providers.id;
CREATE TABLE saved_replies (
id bigint NOT NULL,
user_id bigint NOT NULL,
created_at timestamp with time zone NOT NULL,
updated_at timestamp with time zone NOT NULL,
name text NOT NULL,
content text NOT NULL,
CONSTRAINT check_0cb57dc22a CHECK ((char_length(content) <= 10000)),
CONSTRAINT check_2eb3366d7f CHECK ((char_length(name) <= 255))
);
CREATE SEQUENCE saved_replies_id_seq
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
ALTER SEQUENCE saved_replies_id_seq OWNED BY saved_replies.id;
CREATE TABLE schema_migrations (
version character varying NOT NULL,
finished_at timestamp with time zone DEFAULT now()
@ -22966,6 +22986,8 @@ ALTER TABLE ONLY saml_group_links ALTER COLUMN id SET DEFAULT nextval('saml_grou
ALTER TABLE ONLY saml_providers ALTER COLUMN id SET DEFAULT nextval('saml_providers_id_seq'::regclass);
ALTER TABLE ONLY saved_replies ALTER COLUMN id SET DEFAULT nextval('saved_replies_id_seq'::regclass);
ALTER TABLE ONLY scim_identities ALTER COLUMN id SET DEFAULT nextval('scim_identities_id_seq'::regclass);
ALTER TABLE ONLY scim_oauth_access_tokens ALTER COLUMN id SET DEFAULT nextval('scim_oauth_access_tokens_id_seq'::regclass);
@ -25112,6 +25134,9 @@ ALTER TABLE ONLY saml_group_links
ALTER TABLE ONLY saml_providers
ADD CONSTRAINT saml_providers_pkey PRIMARY KEY (id);
ALTER TABLE ONLY saved_replies
ADD CONSTRAINT saved_replies_pkey PRIMARY KEY (id);
ALTER TABLE ONLY schema_migrations
ADD CONSTRAINT schema_migrations_pkey PRIMARY KEY (version);
@ -28894,6 +28919,8 @@ CREATE UNIQUE INDEX index_saml_group_links_on_group_id_and_saml_group_name ON sa
CREATE INDEX index_saml_providers_on_group_id ON saml_providers USING btree (group_id);
CREATE UNIQUE INDEX index_saved_replies_on_name_text_pattern_ops ON saved_replies USING btree (user_id, name text_pattern_ops);
CREATE INDEX index_scim_identities_on_group_id ON scim_identities USING btree (group_id);
CREATE UNIQUE INDEX index_scim_identities_on_lower_extern_uid_and_group_id ON scim_identities USING btree (lower((extern_uid)::text), group_id);
@ -32789,6 +32816,9 @@ ALTER TABLE ONLY resource_milestone_events
ALTER TABLE ONLY term_agreements
ADD CONSTRAINT fk_rails_a88721bcdf FOREIGN KEY (term_id) REFERENCES application_setting_terms(id);
ALTER TABLE ONLY saved_replies
ADD CONSTRAINT fk_rails_a8bf5bf111 FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
ALTER TABLE ONLY ci_pipeline_artifacts
ADD CONSTRAINT fk_rails_a9e811a466 FOREIGN KEY (pipeline_id) REFERENCES ci_pipelines(id) ON DELETE CASCADE;

View File

@ -19,7 +19,7 @@ You can create an issue in many ways in GitLab:
- [From a project](#from-a-project)
- [From a group](#from-a-group)
- [From another issue](#from-another-issue)
- [From another issue or incident](#from-another-issue-or-incident)
- [From an issue board](#from-an-issue-board)
- [By sending an email](#by-sending-an-email)
- [Using a URL with prefilled values](#using-a-url-with-prefilled-values)
@ -70,9 +70,10 @@ The newly created issue opens.
The project you selected most recently becomes the default for your next visit.
This can save you a lot of time and clicks, if you mostly create issues for the same project.
### From another issue
### From another issue or incident
> New issue becoming linked to the issue of origin [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/68226) in GitLab 14.3.
> - New issue becoming linked to the issue of origin [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/68226) in GitLab 14.3.
> - **Relate to…** checkbox [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/198494) in GitLab 14.9.
You can create a new issue from an existing one. The two issues can then be marked as related.
@ -83,10 +84,10 @@ Prerequisites:
To create an issue from another issue:
1. In an existing issue, select the vertical ellipsis (**{ellipsis_v}**).
1. Select **New issue**.
1. Select **New related issue**.
1. Complete the [fields](#fields-in-the-new-issue-form).
The new issue's description is prefilled with `Related to #123`, where `123` is the ID of the
issue of origin. If you keep this mention in the description, the two issues become
The new issue form has a **Relate to issue #123** checkbox, where `123` is the ID of the
issue of origin. If you keep this checkbox checked, the two issues become
[linked](related_issues.md).
1. Select **Create issue**.
@ -160,7 +161,8 @@ To regenerate the email address:
### Using a URL with prefilled values
> Ability to use both `issuable_template` and `issue[description]` in the same URL [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/80554) in GitLab 14.9.
> - Ability to use both `issuable_template` and `issue[description]` in the same URL [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/80554) in GitLab 14.9.
> - Ability to specify `add_related_issue` [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/198494) in GitLab 14.9.
To link directly to the new issue page with prefilled fields, use query
string parameters in a URL. You can embed a URL in an external
@ -173,6 +175,7 @@ HTML page to create issues with certain fields prefilled.
| Description template | `issuable_template` | Must be [URL-encoded](../../../api/index.md#namespaced-path-encoding). |
| Description | `issue[description]` | Must be [URL-encoded](../../../api/index.md#namespaced-path-encoding). If used in combination with `issuable_template` or a [default issue template](../description_templates.md#set-a-default-template-for-merge-requests-and-issues), the `issue[description]` value is appended to the template. |
| Confidential | `issue[confidential]` | If `true`, the issue is marked as confidential. |
| Relate to… | `add_related_issue` | A numeric issue ID. If present, the issue form shows a [**Relate to…** checkbox](#from-another-issue-or-incident) to optionally link the new issue to the specified existing issue. |
Adapt these examples to form your new issue URL with prefilled fields.
To create an issue in the GitLab project:

View File

@ -460,6 +460,7 @@ reviews: :gitlab_main
routes: :gitlab_main
saml_group_links: :gitlab_main
saml_providers: :gitlab_main
saved_replies: :gitlab_main
schema_migrations: :gitlab_shared
scim_identities: :gitlab_main
scim_oauth_access_tokens: :gitlab_main

View File

@ -459,6 +459,15 @@ module Gitlab
"can contain only lowercase letters, digits, '_' and '-'. " \
"Must start with a letter, and cannot end with '-' or '_'"
end
def saved_reply_name_regex
@saved_reply_name_regex ||= /\A[a-z]([a-z0-9\-_]*[a-z0-9])?\z/.freeze
end
def saved_reply_name_regex_message
"can contain only lowercase letters, digits, '_' and '-'. " \
"Must start with a letter, and cannot end with '-' or '_'"
end
end
end

View File

@ -2394,6 +2394,9 @@ msgstr ""
msgid "Adds email participant(s)."
msgstr ""
msgid "Adds this %{issuable_type} as related to the %{issuable_type} it was created from"
msgstr ""
msgid "Adjust how frequently the GitLab UI polls for updates."
msgstr ""
@ -24337,9 +24340,6 @@ msgstr ""
msgid "New"
msgstr ""
msgid "New %{issueType}"
msgstr ""
msgid "New %{type} in %{project}"
msgstr ""
@ -24507,6 +24507,9 @@ msgstr ""
msgid "New public deploy key"
msgstr ""
msgid "New related %{issueType}"
msgstr ""
msgid "New release"
msgstr ""
@ -30313,6 +30316,9 @@ msgstr ""
msgid "Rejected (closed)"
msgstr ""
msgid "Relate to %{issuable_type} %{add_related_issue_link}"
msgstr ""
msgid "Related feature flags"
msgstr ""
@ -30322,9 +30328,6 @@ msgstr ""
msgid "Related merge requests"
msgstr ""
msgid "Related to #%{issue_id}."
msgstr ""
msgid "Relates to"
msgstr ""

View File

@ -0,0 +1,10 @@
# frozen_string_literal: true
FactoryBot.define do
factory :saved_reply, class: 'Users::SavedReply' do
sequence(:name) { |n| "saved_reply_#{n}" }
content { 'Saved Reply Content' }
user
end
end

View File

@ -26,7 +26,7 @@ RSpec.describe "User views incident" do
it 'shows the merge request and incident actions', :js, :aggregate_failures do
click_button 'Incident actions'
expect(page).to have_link('New incident', href: new_project_issue_path(project, { issuable_template: 'incident', issue: { issue_type: 'incident', description: "Related to \##{incident.iid}.\n\n" } }))
expect(page).to have_link('New related incident', href: new_project_issue_path(project, { issuable_template: 'incident', issue: { issue_type: 'incident' }, add_related_issue: incident.iid }))
expect(page).to have_button('Create merge request')
expect(page).to have_button('Close incident')
end

View File

@ -8,16 +8,19 @@ RSpec.describe 'New/edit issue', :js do
let_it_be(:project) { create(:project, :repository) }
let_it_be(:user) { create(:user) }
let_it_be(:user2) { create(:user) }
let_it_be(:guest) { create(:user) }
let_it_be(:milestone) { create(:milestone, project: project) }
let_it_be(:label) { create(:label, project: project) }
let_it_be(:label2) { create(:label, project: project) }
let_it_be(:issue) { create(:issue, project: project, assignees: [user], milestone: milestone) }
let_it_be(:confidential_issue) { create(:issue, project: project, assignees: [user], milestone: milestone, confidential: true) }
let(:current_user) { user }
before_all do
project.add_maintainer(user)
project.add_maintainer(user2)
project.add_guest(guest)
end
before do
@ -357,6 +360,61 @@ RSpec.describe 'New/edit issue', :js do
end
end
describe 'new issue from related issue' do
it 'does not offer to link the new issue to any other issues if the URL parameter is absent' do
visit new_project_issue_path(project)
expect(page).not_to have_selector '#add_related_issue'
expect(page).not_to have_text "Relate to"
end
context 'guest' do
let(:current_user) { guest }
it 'does not offer to link the new issue to an issue that the user does not have access to' do
visit new_project_issue_path(project, { add_related_issue: confidential_issue.iid })
expect(page).not_to have_selector '#add_related_issue'
expect(page).not_to have_text "Relate to"
end
end
it 'links the new issue and the issue of origin' do
visit new_project_issue_path(project, { add_related_issue: issue.iid })
expect(page).to have_selector '#add_related_issue'
expect(page).to have_text "Relate to issue \##{issue.iid}"
expect(page).to have_text 'Adds this issue as related to the issue it was created from'
fill_in 'issue_title', with: 'title'
click_button 'Create issue'
page.within '#related-issues' do
expect(page).to have_text "\##{issue.iid}"
end
end
it 'links the new incident and the incident of origin' do
incident = create(:incident, project: project)
visit new_project_issue_path(project, { add_related_issue: incident.iid })
expect(page).to have_selector '#add_related_issue'
expect(page).to have_text "Relate to incident \##{incident.iid}"
expect(page).to have_text 'Adds this incident as related to the incident it was created from'
fill_in 'issue_title', with: 'title'
click_button 'Create issue'
page.within '#related-issues' do
expect(page).to have_text "\##{incident.iid}"
end
end
it 'does not link the new issue to any other issues if the checkbox is not checked' do
visit new_project_issue_path(project, { add_related_issue: issue.iid })
expect(page).to have_selector '#add_related_issue'
expect(page).to have_text "Relate to issue \##{issue.iid}"
uncheck "Relate to issue \##{issue.iid}"
fill_in 'issue_title', with: 'title'
click_button 'Create issue'
page.within '#related-issues' do
expect(page).not_to have_text "\##{issue.iid}"
end
end
end
describe 'edit issue' do
before do
visit edit_project_issue_path(project, issue)

View File

@ -25,8 +25,8 @@ RSpec.describe 'issue header', :js do
click_button 'Issue actions'
end
it 'shows the "New issue", "Report abuse", and "Delete issue" items', :aggregate_failures do
expect(page).to have_link 'New issue'
it 'shows the "New related issue", "Report abuse", and "Delete issue" items', :aggregate_failures do
expect(page).to have_link 'New related issue'
expect(page).to have_link 'Report abuse'
expect(page).to have_button 'Delete issue'
expect(page).not_to have_link 'Submit as spam'
@ -114,8 +114,8 @@ RSpec.describe 'issue header', :js do
click_button 'Issue actions'
end
it 'only shows the "New issue" and "Report abuse" items', :aggregate_failures do
expect(page).to have_link 'New issue'
it 'only shows the "New related issue" and "Report abuse" items', :aggregate_failures do
expect(page).to have_link 'New related issue'
expect(page).to have_link 'Report abuse'
expect(page).not_to have_link 'Submit as spam'
expect(page).not_to have_button 'Delete issue'

View File

@ -25,7 +25,7 @@ RSpec.describe "User views issue" do
it 'shows the merge request and issue actions', :js, :aggregate_failures do
click_button 'Issue actions'
expect(page).to have_link('New issue', href: new_project_issue_path(project, { issue: { description: "Related to \##{issue.iid}.\n\n" } }))
expect(page).to have_link('New related issue', href: new_project_issue_path(project, { add_related_issue: issue.iid }))
expect(page).to have_button('Create merge request')
expect(page).to have_button('Close issue')
end

View File

@ -26,7 +26,7 @@ RSpec.describe 'Sandboxed Mermaid rendering', :js do
wait_for_requests
expected = %(<iframe src="/-/sandbox/mermaid" sandbox="allow-scripts" frameborder="0" scrolling="no")
expected = %(<iframe src="/-/sandbox/mermaid" sandbox="allow-scripts allow-popups" frameborder="0" scrolling="no")
expect(page.html).to include(expected)
end
end

View File

@ -171,19 +171,19 @@ describe('HeaderActions component', () => {
${'desktop dropdown'} | ${false} | ${findDesktopDropdownItems} | ${findDesktopDropdown}
`('$description', ({ isCloseIssueItemVisible, findDropdownItems, findDropdown }) => {
describe.each`
description | itemText | isItemVisible | canUpdateIssue | canCreateIssue | isIssueAuthor | canReportSpam | canPromoteToEpic | canDestroyIssue
${`when user can update ${issueType}`} | ${`Close ${issueType}`} | ${isCloseIssueItemVisible} | ${true} | ${true} | ${true} | ${true} | ${true} | ${true}
${`when user cannot update ${issueType}`} | ${`Close ${issueType}`} | ${false} | ${false} | ${true} | ${true} | ${true} | ${true} | ${true}
${`when user can create ${issueType}`} | ${`New ${issueType}`} | ${true} | ${true} | ${true} | ${true} | ${true} | ${true} | ${true}
${`when user cannot create ${issueType}`} | ${`New ${issueType}`} | ${false} | ${true} | ${false} | ${true} | ${true} | ${true} | ${true}
${'when user can promote to epic'} | ${'Promote to epic'} | ${true} | ${true} | ${true} | ${true} | ${true} | ${true} | ${true}
${'when user cannot promote to epic'} | ${'Promote to epic'} | ${false} | ${true} | ${true} | ${true} | ${true} | ${false} | ${true}
${'when user can report abuse'} | ${'Report abuse'} | ${true} | ${true} | ${true} | ${false} | ${true} | ${true} | ${true}
${'when user cannot report abuse'} | ${'Report abuse'} | ${false} | ${true} | ${true} | ${true} | ${true} | ${true} | ${true}
${'when user can submit as spam'} | ${'Submit as spam'} | ${true} | ${true} | ${true} | ${true} | ${true} | ${true} | ${true}
${'when user cannot submit as spam'} | ${'Submit as spam'} | ${false} | ${true} | ${true} | ${true} | ${false} | ${true} | ${true}
${`when user can delete ${issueType}`} | ${`Delete ${issueType}`} | ${true} | ${true} | ${true} | ${true} | ${true} | ${true} | ${true}
${`when user cannot delete ${issueType}`} | ${`Delete ${issueType}`} | ${false} | ${true} | ${true} | ${true} | ${true} | ${true} | ${false}
description | itemText | isItemVisible | canUpdateIssue | canCreateIssue | isIssueAuthor | canReportSpam | canPromoteToEpic | canDestroyIssue
${`when user can update ${issueType}`} | ${`Close ${issueType}`} | ${isCloseIssueItemVisible} | ${true} | ${true} | ${true} | ${true} | ${true} | ${true}
${`when user cannot update ${issueType}`} | ${`Close ${issueType}`} | ${false} | ${false} | ${true} | ${true} | ${true} | ${true} | ${true}
${`when user can create ${issueType}`} | ${`New related ${issueType}`} | ${true} | ${true} | ${true} | ${true} | ${true} | ${true} | ${true}
${`when user cannot create ${issueType}`} | ${`New related ${issueType}`} | ${false} | ${true} | ${false} | ${true} | ${true} | ${true} | ${true}
${'when user can promote to epic'} | ${'Promote to epic'} | ${true} | ${true} | ${true} | ${true} | ${true} | ${true} | ${true}
${'when user cannot promote to epic'} | ${'Promote to epic'} | ${false} | ${true} | ${true} | ${true} | ${true} | ${false} | ${true}
${'when user can report abuse'} | ${'Report abuse'} | ${true} | ${true} | ${true} | ${false} | ${true} | ${true} | ${true}
${'when user cannot report abuse'} | ${'Report abuse'} | ${false} | ${true} | ${true} | ${true} | ${true} | ${true} | ${true}
${'when user can submit as spam'} | ${'Submit as spam'} | ${true} | ${true} | ${true} | ${true} | ${true} | ${true} | ${true}
${'when user cannot submit as spam'} | ${'Submit as spam'} | ${false} | ${true} | ${true} | ${true} | ${false} | ${true} | ${true}
${`when user can delete ${issueType}`} | ${`Delete ${issueType}`} | ${true} | ${true} | ${true} | ${true} | ${true} | ${true} | ${true}
${`when user cannot delete ${issueType}`} | ${`Delete ${issueType}`} | ${false} | ${true} | ${true} | ${true} | ${true} | ${true} | ${false}
`(
'$description',
({

View File

@ -265,7 +265,7 @@ RSpec.describe IssuesHelper do
is_issue_author: 'false',
issue_path: issue_path(issue),
issue_type: 'issue',
new_issue_path: new_project_issue_path(project, { issue: { description: "Related to \##{issue.iid}.\n\n" } }),
new_issue_path: new_project_issue_path(project, { add_related_issue: issue.iid }),
project_path: project.full_path,
report_abuse_path: new_abuse_report_path(user_id: issue.author.id, ref_url: issue_url(issue)),
submit_as_spam_path: mark_as_spam_project_issue_path(project, issue)

View File

@ -990,4 +990,19 @@ RSpec.describe Gitlab::Regex do
it { is_expected.not_to match('../../../../../1.2.3') }
it { is_expected.not_to match('%2e%2e%2f1.2.3') }
end
describe '.saved_reply_name_regex' do
subject { described_class.saved_reply_name_regex }
it { is_expected.to match('test') }
it { is_expected.to match('test123') }
it { is_expected.to match('test-test') }
it { is_expected.to match('test-test_0123') }
it { is_expected.not_to match('test test') }
it { is_expected.not_to match('test-') }
it { is_expected.not_to match('/z/test_') }
it { is_expected.not_to match('.xtest_') }
it { is_expected.not_to match('.xt.est_') }
it { is_expected.not_to match('0test1') }
end
end

View File

@ -116,6 +116,7 @@ RSpec.describe User do
it { is_expected.to have_many(:builds) }
it { is_expected.to have_many(:pipelines) }
it { is_expected.to have_many(:chat_names).dependent(:destroy) }
it { is_expected.to have_many(:saved_replies).class_name('::Users::SavedReply') }
it { is_expected.to have_many(:uploads) }
it { is_expected.to have_many(:reported_abuse_reports).dependent(:destroy).class_name('AbuseReport') }
it { is_expected.to have_many(:custom_attributes).class_name('UserCustomAttribute') }

View File

@ -0,0 +1,16 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Users::SavedReply do
let_it_be(:saved_reply) { create(:saved_reply) }
describe 'validations' do
it { is_expected.to validate_presence_of(:user_id) }
it { is_expected.to validate_presence_of(:name) }
it { is_expected.to validate_presence_of(:content) }
it { is_expected.to validate_uniqueness_of(:name).scoped_to([:user_id]) }
it { is_expected.to validate_length_of(:name).is_at_most(255) }
it { is_expected.to validate_length_of(:content).is_at_most(10000) }
end
end

View File

@ -526,6 +526,31 @@ RSpec.describe Issues::CreateService do
end
end
context 'add related issue' do
let_it_be(:related_issue) { create(:issue, project: project) }
let(:opts) do
{ title: 'A new issue', add_related_issue: related_issue }
end
it 'ignores related issue if not accessible' do
expect { issue }.not_to change { IssueLink.count }
expect(issue).to be_persisted
end
context 'when user has access to the related issue' do
before do
project.add_developer(user)
end
it 'adds a link to the issue' do
expect { issue }.to change { IssueLink.count }.by(1)
expect(issue).to be_persisted
expect(issue.related_issues(user)).to eq([related_issue])
end
end
end
context 'checking spam' do
let(:params) do
{