From 1e28c9498f80fa09b3061ef0903cf99e5142c2f2 Mon Sep 17 00:00:00 2001 From: GitLab Bot Date: Thu, 3 Mar 2022 18:19:46 +0000 Subject: [PATCH] Add latest changes from gitlab-org/gitlab@master --- .gitpod.yml | 4 ++ .../markdown/render_sandboxed_mermaid.js | 2 +- .../filtered_search_dropdown.js | 3 +- .../issues/show/components/header_actions.vue | 2 +- .../webpack_non_compiled_placeholder.js | 15 ++++- .../stylesheets/startup/startup-dark.scss | 13 ----- .../stylesheets/startup/startup-general.scss | 13 ----- .../stylesheets/startup/startup-signin.scss | 13 ----- app/controllers/projects/issues_controller.rb | 8 +++ app/helpers/appearances_helper.rb | 2 + app/helpers/issues_helper.rb | 2 +- app/models/user.rb | 1 + app/models/users/saved_reply.rb | 19 ++++++ app/services/issues/create_service.rb | 8 +++ app/views/shared/_logo_ukraine.svg | 5 ++ .../shared/issuable/form/_metadata.html.haml | 10 ++++ .../development/ukraine_support_tanuki.yml | 8 +++ .../incremental_webpack_compiler/compiler.js | 2 +- config/webpack.config.js | 1 + .../20220216110023_create_saved_replies.rb | 20 +++++++ db/schema_migrations/20220216110023 | 1 + db/structure.sql | 30 ++++++++++ doc/user/project/issues/managing_issues.md | 17 +++--- lib/gitlab/database/gitlab_schemas.yml | 1 + lib/gitlab/regex.rb | 9 +++ locale/gitlab.pot | 15 +++-- spec/factories/users/saved_replies.rb | 10 ++++ .../incidents/user_views_incident_spec.rb | 2 +- spec/features/issues/form_spec.rb | 58 +++++++++++++++++++ spec/features/issues/issue_header_spec.rb | 8 +-- spec/features/issues/user_views_issue_spec.rb | 2 +- .../markdown/sandboxed_mermaid_spec.rb | 2 +- .../show/components/header_actions_spec.js | 26 ++++----- spec/helpers/issues_helper_spec.rb | 2 +- spec/lib/gitlab/regex_spec.rb | 15 +++++ spec/models/user_spec.rb | 1 + spec/models/users/saved_reply_spec.rb | 16 +++++ spec/services/issues/create_service_spec.rb | 25 ++++++++ 38 files changed, 311 insertions(+), 80 deletions(-) create mode 100644 app/models/users/saved_reply.rb create mode 100644 app/views/shared/_logo_ukraine.svg create mode 100644 config/feature_flags/development/ukraine_support_tanuki.yml create mode 100644 db/migrate/20220216110023_create_saved_replies.rb create mode 100644 db/schema_migrations/20220216110023 create mode 100644 spec/factories/users/saved_replies.rb create mode 100644 spec/models/users/saved_reply_spec.rb diff --git a/.gitpod.yml b/.gitpod.yml index a67242e08a5..d1a709c55ea 100644 --- a/.gitpod.yml +++ b/.gitpod.yml @@ -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 diff --git a/app/assets/javascripts/behaviors/markdown/render_sandboxed_mermaid.js b/app/assets/javascripts/behaviors/markdown/render_sandboxed_mermaid.js index 1d54a1b0c04..85a991a1ec9 100644 --- a/app/assets/javascripts/behaviors/markdown/render_sandboxed_mermaid.js +++ b/app/assets/javascripts/behaviors/markdown/render_sandboxed_mermaid.js @@ -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%', diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js index fcc7caa9ff2..9de291b7809 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js +++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js @@ -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 = `
- + ${loadingIconForLegacyJS().outerHTML}
`; this.bindEvents(); } diff --git a/app/assets/javascripts/issues/show/components/header_actions.vue b/app/assets/javascripts/issues/show/components/header_actions.vue index 78ec5071b59..92f0948ec48 100644 --- a/app/assets/javascripts/issues/show/components/header_actions.vue +++ b/app/assets/javascripts/issues/show/components/header_actions.vue @@ -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; diff --git a/app/assets/javascripts/webpack_non_compiled_placeholder.js b/app/assets/javascripts/webpack_non_compiled_placeholder.js index 55ac2f0be6a..af671e72129 100644 --- a/app/assets/javascripts/webpack_non_compiled_placeholder.js +++ b/app/assets/javascripts/webpack_non_compiled_placeholder.js @@ -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 = ` @@ -30,9 +35,15 @@ div.innerHTML = ` Learn more here.

- If you have live_reload enabled, the page will reload automatically when complete.
- Otherwise, please reload the page manually in a few seconds + ${reloadMessage}
+ If it doesn't, please reload the page manually.

`; document.body.append(div); + +if (!LIVE_RELOAD) { + setTimeout(() => { + window.location.reload(); + }, 5000); +} diff --git a/app/assets/stylesheets/startup/startup-dark.scss b/app/assets/stylesheets/startup/startup-dark.scss index 684808e748d..81a934babfe 100644 --- a/app/assets/stylesheets/startup/startup-dark.scss +++ b/app/assets/stylesheets/startup/startup-dark.scss @@ -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; diff --git a/app/assets/stylesheets/startup/startup-general.scss b/app/assets/stylesheets/startup/startup-general.scss index 0d35c400676..afeaf16e4b6 100644 --- a/app/assets/stylesheets/startup/startup-general.scss +++ b/app/assets/stylesheets/startup/startup-general.scss @@ -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; diff --git a/app/assets/stylesheets/startup/startup-signin.scss b/app/assets/stylesheets/startup/startup-signin.scss index c5cbe58ec27..213d1c013a0 100644 --- a/app/assets/stylesheets/startup/startup-signin.scss +++ b/app/assets/stylesheets/startup/startup-signin.scss @@ -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; diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index c8b1ed04e4a..cd0311d0bac 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -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 diff --git a/app/helpers/appearances_helper.rb b/app/helpers/appearances_helper.rb index 5ca360f38da..cb43d911a2f 100644 --- a/app/helpers/appearances_helper.rb +++ b/app/helpers/appearances_helper.rb @@ -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 diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb index 8e7f5060412..9187a3eda0a 100644 --- a/app/helpers/issues_helper.rb +++ b/app/helpers/issues_helper.rb @@ -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' diff --git a/app/models/user.rb b/app/models/user.rb index 89a6bd0f008..e49a39079ef 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -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' diff --git a/app/models/users/saved_reply.rb b/app/models/users/saved_reply.rb new file mode 100644 index 00000000000..7737d826b05 --- /dev/null +++ b/app/models/users/saved_reply.rb @@ -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 diff --git a/app/services/issues/create_service.rb b/app/services/issues/create_service.rb index 7fbf7c6af58..7ab663718db 100644 --- a/app/services/issues/create_service.rb +++ b/app/services/issues/create_service.rb @@ -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 diff --git a/app/views/shared/_logo_ukraine.svg b/app/views/shared/_logo_ukraine.svg new file mode 100644 index 00000000000..e2c2bb3855d --- /dev/null +++ b/app/views/shared/_logo_ukraine.svg @@ -0,0 +1,5 @@ + \ No newline at end of file diff --git a/app/views/shared/issuable/form/_metadata.html.haml b/app/views/shared/issuable/form/_metadata.html.haml index 9e42c528a11..34720576526 100644 --- a/app/views/shared/issuable/form/_metadata.html.haml +++ b/app/views/shared/issuable/form/_metadata.html.haml @@ -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 diff --git a/config/feature_flags/development/ukraine_support_tanuki.yml b/config/feature_flags/development/ukraine_support_tanuki.yml new file mode 100644 index 00000000000..3a2c64a5aa4 --- /dev/null +++ b/config/feature_flags/development/ukraine_support_tanuki.yml @@ -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 diff --git a/config/helpers/incremental_webpack_compiler/compiler.js b/config/helpers/incremental_webpack_compiler/compiler.js index 0ef090bce24..2614501faa4 100644 --- a/config/helpers/incremental_webpack_compiler/compiler.js +++ b/config/helpers/incremental_webpack_compiler/compiler.js @@ -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'); } }); diff --git a/config/webpack.config.js b/config/webpack.config.js index b8bc33f5d07..66c02dcc87d 100644 --- a/config/webpack.config.js +++ b/config/webpack.config.js @@ -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. diff --git a/db/migrate/20220216110023_create_saved_replies.rb b/db/migrate/20220216110023_create_saved_replies.rb new file mode 100644 index 00000000000..e4b6c039dee --- /dev/null +++ b/db/migrate/20220216110023_create_saved_replies.rb @@ -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 diff --git a/db/schema_migrations/20220216110023 b/db/schema_migrations/20220216110023 new file mode 100644 index 00000000000..30acd6fdaf2 --- /dev/null +++ b/db/schema_migrations/20220216110023 @@ -0,0 +1 @@ +5931c4981c89d65c5aaca05dc8375c2c21bb595e28354d6623986d906ece165d \ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index 29a6d140d1f..c66d1ccbbf8 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -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; diff --git a/doc/user/project/issues/managing_issues.md b/doc/user/project/issues/managing_issues.md index 1e474775b58..2dc2d4960b7 100644 --- a/doc/user/project/issues/managing_issues.md +++ b/doc/user/project/issues/managing_issues.md @@ -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: diff --git a/lib/gitlab/database/gitlab_schemas.yml b/lib/gitlab/database/gitlab_schemas.yml index 95005871194..4c7e0a4e613 100644 --- a/lib/gitlab/database/gitlab_schemas.yml +++ b/lib/gitlab/database/gitlab_schemas.yml @@ -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 diff --git a/lib/gitlab/regex.rb b/lib/gitlab/regex.rb index a6491d23bf5..ddb6bc37bba 100644 --- a/lib/gitlab/regex.rb +++ b/lib/gitlab/regex.rb @@ -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 diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 337cf832ee7..c6cf57d5005 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -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 "" diff --git a/spec/factories/users/saved_replies.rb b/spec/factories/users/saved_replies.rb new file mode 100644 index 00000000000..a3c450fb1f1 --- /dev/null +++ b/spec/factories/users/saved_replies.rb @@ -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 diff --git a/spec/features/incidents/user_views_incident_spec.rb b/spec/features/incidents/user_views_incident_spec.rb index e0261ad4d0b..a669966502e 100644 --- a/spec/features/incidents/user_views_incident_spec.rb +++ b/spec/features/incidents/user_views_incident_spec.rb @@ -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 diff --git a/spec/features/issues/form_spec.rb b/spec/features/issues/form_spec.rb index d67562e32fc..18942e48400 100644 --- a/spec/features/issues/form_spec.rb +++ b/spec/features/issues/form_spec.rb @@ -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) diff --git a/spec/features/issues/issue_header_spec.rb b/spec/features/issues/issue_header_spec.rb index 3e27ce81860..165015013dd 100644 --- a/spec/features/issues/issue_header_spec.rb +++ b/spec/features/issues/issue_header_spec.rb @@ -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' diff --git a/spec/features/issues/user_views_issue_spec.rb b/spec/features/issues/user_views_issue_spec.rb index 31bf7649470..eca698bb2f4 100644 --- a/spec/features/issues/user_views_issue_spec.rb +++ b/spec/features/issues/user_views_issue_spec.rb @@ -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 diff --git a/spec/features/markdown/sandboxed_mermaid_spec.rb b/spec/features/markdown/sandboxed_mermaid_spec.rb index f118fb3db66..05fe83b3107 100644 --- a/spec/features/markdown/sandboxed_mermaid_spec.rb +++ b/spec/features/markdown/sandboxed_mermaid_spec.rb @@ -26,7 +26,7 @@ RSpec.describe 'Sandboxed Mermaid rendering', :js do wait_for_requests - expected = %(