Commit 768263f3 authored by Marcin Sedlak-Jakubowski's avatar Marcin Sedlak-Jakubowski

Merge branch '15694-allow-expanded-gfm-references' into 'master'

Allow issuable references to show titles

See merge request gitlab-org/gitlab!74369
parents 99594281 cdf83616
...@@ -2539,12 +2539,10 @@ Rails/IncludeUrlHelper: ...@@ -2539,12 +2539,10 @@ Rails/IncludeUrlHelper:
- 'ee/app/presenters/merge_request_approver_presenter.rb' - 'ee/app/presenters/merge_request_approver_presenter.rb'
- 'ee/spec/helpers/ee/projects/security/configuration_helper_spec.rb' - 'ee/spec/helpers/ee/projects/security/configuration_helper_spec.rb'
- 'ee/spec/lib/banzai/filter/cross_project_issuable_information_filter_spec.rb' - 'ee/spec/lib/banzai/filter/cross_project_issuable_information_filter_spec.rb'
- 'ee/spec/lib/banzai/filter/issuable_state_filter_spec.rb'
- 'lib/gitlab/ci/badge/metadata.rb' - 'lib/gitlab/ci/badge/metadata.rb'
- 'spec/helpers/merge_requests_helper_spec.rb' - 'spec/helpers/merge_requests_helper_spec.rb'
- 'spec/helpers/nav/top_nav_helper_spec.rb' - 'spec/helpers/nav/top_nav_helper_spec.rb'
- 'spec/helpers/notify_helper_spec.rb' - 'spec/helpers/notify_helper_spec.rb'
- 'spec/lib/banzai/filter/issuable_state_filter_spec.rb'
- 'spec/lib/banzai/filter/reference_redactor_filter_spec.rb' - 'spec/lib/banzai/filter/reference_redactor_filter_spec.rb'
- 'spec/lib/banzai/reference_redactor_spec.rb' - 'spec/lib/banzai/reference_redactor_spec.rb'
......
...@@ -21,7 +21,7 @@ module PreviewMarkdown ...@@ -21,7 +21,7 @@ module PreviewMarkdown
def projects_filter_params def projects_filter_params
{ {
issuable_state_filter_enabled: true, issuable_reference_expansion_enabled: true,
suggestions_filter_enabled: params[:preview_suggestions].present? suggestions_filter_enabled: params[:preview_suggestions].present?
} }
end end
......
...@@ -110,7 +110,7 @@ class Projects::MergeRequests::DraftsController < Projects::MergeRequests::Appli ...@@ -110,7 +110,7 @@ class Projects::MergeRequests::DraftsController < Projects::MergeRequests::Appli
def render_draft_note(note) def render_draft_note(note)
params = { target_id: merge_request.id, target_type: 'MergeRequest', text: note.note } params = { target_id: merge_request.id, target_type: 'MergeRequest', text: note.note }
result = PreviewMarkdownService.new(@project, current_user, params).execute result = PreviewMarkdownService.new(@project, current_user, params).execute
markdown_params = { markdown_engine: result[:markdown_engine], issuable_state_filter_enabled: true } markdown_params = { markdown_engine: result[:markdown_engine], issuable_reference_expansion_enabled: true }
note.rendered_note = view_context.markdown(result[:text], markdown_params) note.rendered_note = view_context.markdown(result[:text], markdown_params)
note.users_referenced = result[:users] note.users_referenced = result[:users]
......
...@@ -181,7 +181,7 @@ module MarkupHelper ...@@ -181,7 +181,7 @@ module MarkupHelper
wiki: wiki, wiki: wiki,
repository: wiki.repository, repository: wiki.repository,
page_slug: wiki_page.slug, page_slug: wiki_page.slug,
issuable_state_filter_enabled: true issuable_reference_expansion_enabled: true
).merge(render_wiki_content_context_container(wiki)) ).merge(render_wiki_content_context_container(wiki))
end end
......
...@@ -43,7 +43,7 @@ module Issuable ...@@ -43,7 +43,7 @@ module Issuable
included do included do
cache_markdown_field :title, pipeline: :single_line cache_markdown_field :title, pipeline: :single_line
cache_markdown_field :description, issuable_state_filter_enabled: true cache_markdown_field :description, issuable_reference_expansion_enabled: true
redact_field :description redact_field :description
......
...@@ -506,7 +506,7 @@ class MergeRequest < ApplicationRecord ...@@ -506,7 +506,7 @@ class MergeRequest < ApplicationRecord
def self.reference_pattern def self.reference_pattern
@reference_pattern ||= %r{ @reference_pattern ||= %r{
(#{Project.reference_pattern})? (#{Project.reference_pattern})?
#{Regexp.escape(reference_prefix)}(?<merge_request>\d+) #{Regexp.escape(reference_prefix)}(?<merge_request>\d+)(?<format>\+)?
}x }x
end end
......
...@@ -23,7 +23,7 @@ class Note < ApplicationRecord ...@@ -23,7 +23,7 @@ class Note < ApplicationRecord
include FromUnion include FromUnion
include Sortable include Sortable
cache_markdown_field :note, pipeline: :note, issuable_state_filter_enabled: true cache_markdown_field :note, pipeline: :note, issuable_reference_expansion_enabled: true
redact_field :note redact_field :note
......
...@@ -136,7 +136,7 @@ class MergeRequestPresenter < Gitlab::View::Presenter::Delegated ...@@ -136,7 +136,7 @@ class MergeRequestPresenter < Gitlab::View::Presenter::Delegated
pipeline: :gfm, pipeline: :gfm,
author: author, author: author,
project: project, project: project,
issuable_state_filter_enabled: true issuable_reference_expansion_enabled: true
) )
end end
...@@ -146,7 +146,7 @@ class MergeRequestPresenter < Gitlab::View::Presenter::Delegated ...@@ -146,7 +146,7 @@ class MergeRequestPresenter < Gitlab::View::Presenter::Delegated
pipeline: :gfm, pipeline: :gfm,
author: author, author: author,
project: project, project: project,
issuable_state_filter_enabled: true issuable_reference_expansion_enabled: true
) )
end end
......
...@@ -517,7 +517,7 @@ version to reference other projects from the same namespace. ...@@ -517,7 +517,7 @@ version to reference other projects from the same namespace.
GitLab Flavored Markdown recognizes the following: GitLab Flavored Markdown recognizes the following:
| references | input | cross-project reference | shortcut inside same namespace | | references | input | cross-project reference | shortcut inside same namespace |
| :------------------------------ | :------------------------- | :-------------------------------------- | :----------------------------- | | :--------------------------------------------------- | :---------------------------- | :----------------------------------------- | :------------------------------- |
| specific user | `@user_name` | | | | specific user | `@user_name` | | |
| specific group | `@group_name` | | | | specific group | `@group_name` | | |
| entire team | `@all` | | | | entire team | `@all` | | |
...@@ -525,8 +525,8 @@ GitLab Flavored Markdown recognizes the following: ...@@ -525,8 +525,8 @@ GitLab Flavored Markdown recognizes the following:
| issue | ``#123`` | `namespace/project#123` | `project#123` | | issue | ``#123`` | `namespace/project#123` | `project#123` |
| merge request | `!123` | `namespace/project!123` | `project!123` | | merge request | `!123` | `namespace/project!123` | `project!123` |
| snippet | `$123` | `namespace/project$123` | `project$123` | | snippet | `$123` | `namespace/project$123` | `project$123` |
| epic **(ULTIMATE)** | `&123` | `group1/subgroup&123` | | | [epic](group/epics/index.md) | `&123` | `group1/subgroup&123` | |
| vulnerability **(ULTIMATE)** (1)| `[vulnerability:123]` | `[vulnerability:namespace/project/123]` | `[vulnerability:project/123]` | | vulnerability **(ULTIMATE)** <sup>1</sup> | `[vulnerability:123]` | `[vulnerability:namespace/project/123]` | `[vulnerability:project/123]` |
| feature flag | `[feature_flag:123]` | `[feature_flag:namespace/project/123]` | `[feature_flag:project/123]` | | feature flag | `[feature_flag:123]` | `[feature_flag:namespace/project/123]` | `[feature_flag:project/123]` |
| label by ID | `~123` | `namespace/project~123` | `project~123` | | label by ID | `~123` | `namespace/project~123` | `project~123` |
| one-word label by name | `~bug` | `namespace/project~bug` | `project~bug` | | one-word label by name | `~bug` | `namespace/project~bug` | `project~bug` |
...@@ -554,6 +554,16 @@ In addition to this, links to some objects are also recognized and formatted. So ...@@ -554,6 +554,16 @@ In addition to this, links to some objects are also recognized and formatted. So
- The issues designs tab: `"https://gitlab.com/gitlab-org/gitlab/-/issues/1234/designs"`, which are rendered as `#1234 (designs)`. - The issues designs tab: `"https://gitlab.com/gitlab-org/gitlab/-/issues/1234/designs"`, which are rendered as `#1234 (designs)`.
- Links to individual designs: `"https://gitlab.com/gitlab-org/gitlab/-/issues/1234/designs/layout.png"`, which are rendered as `#1234[layout.png]`. - Links to individual designs: `"https://gitlab.com/gitlab-org/gitlab/-/issues/1234/designs/layout.png"`, which are rendered as `#1234[layout.png]`.
### Show the issue, merge request, or epic title in the reference
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/15694) in GitLab 14.6.
To include the title in the rendered link of an issue, merge request, or epic, add a plus (`+`)
at the end of the reference. For example, a reference like `#123+` is rendered as
`The issue title (#123)`.
Expanding titles does not apply to URL references, like `https://gitlab.com/gitlab-org/gitlab/-/issues/1234`.
### Embedding metrics in GitLab Flavored Markdown ### Embedding metrics in GitLab Flavored Markdown
Metric charts can be embedded in GitLab Flavored Markdown. Read Metric charts can be embedded in GitLab Flavored Markdown. Read
......
...@@ -221,7 +221,7 @@ module EE ...@@ -221,7 +221,7 @@ module EE
}x }x
%r{ %r{
(#{group_regexp})? (#{group_regexp})?
(?:#{combined_prefix})(?<epic>\d+) (?:#{combined_prefix})(?<epic>\d+)(?<format>\+)?
}x }x
end end
end end
......
...@@ -23,7 +23,7 @@ module EE ...@@ -23,7 +23,7 @@ module EE
PASSIVE_STATES = %w(dismissed resolved).freeze PASSIVE_STATES = %w(dismissed resolved).freeze
cache_markdown_field :title, pipeline: :single_line cache_markdown_field :title, pipeline: :single_line
cache_markdown_field :description, issuable_state_filter_enabled: true cache_markdown_field :description, issuable_reference_expansion_enabled: true
strip_attributes! :title strip_attributes! :title
......
...@@ -14,7 +14,7 @@ module RequirementsManagement ...@@ -14,7 +14,7 @@ module RequirementsManagement
self.table_name = 'requirements' self.table_name = 'requirements'
cache_markdown_field :title, pipeline: :single_line cache_markdown_field :title, pipeline: :single_line
cache_markdown_field :description, issuable_state_filter_enabled: true cache_markdown_field :description, issuable_reference_expansion_enabled: true
strip_attributes! :title strip_attributes! :title
......
# frozen_string_literal: true # frozen_string_literal: true
require 'spec_helper' require 'spec_helper'
RSpec.describe Banzai::Filter::IssuableStateFilter do RSpec.describe Banzai::Filter::IssuableReferenceExpansionFilter do
include ActionView::Helpers::UrlHelper
include FilterSpecHelper include FilterSpecHelper
let(:user) { create(:user) } let_it_be(:user) { create(:user) }
let(:context) { { current_user: user, issuable_state_filter_enabled: true, group: group } } let_it_be(:group) { create(:group) }
let(:epic) { create(:epic, :opened, group: group) } let_it_be(:other_group) { create(:group) }
let(:closed_epic) { create(:epic, :closed, group: group) } let_it_be(:epic) { create(:epic, :opened, group: group, title: 'Some epic') }
let(:group) { create(:group) } let_it_be(:closed_epic) { create(:epic, :closed, group: group) }
let(:other_group) { create(:group) }
let_it_be(:context) { { current_user: user, issuable_reference_expansion_enabled: true, group: group } }
def create_link(text, data) def create_link(text, data)
link_to(text, '', class: 'gfm has-tooltip', data: data) ActionController::Base.helpers.link_to(text, '', class: 'gfm has-tooltip', data: data)
end end
it 'ignores open epic references' do it 'ignores open epic references' do
...@@ -41,4 +41,12 @@ RSpec.describe Banzai::Filter::IssuableStateFilter do ...@@ -41,4 +41,12 @@ RSpec.describe Banzai::Filter::IssuableStateFilter do
expect(doc.css('a').last.text).to eq("#{closed_epic.to_reference(other_group)}") expect(doc.css('a').last.text).to eq("#{closed_epic.to_reference(other_group)}")
end end
it 'shows title for references with +' do
link = create_link(epic.to_reference, epic: epic.id, reference_type: 'epic', reference_format: '+')
doc = filter(link, context)
expect(doc.css('a').last.text).to eq("#{epic.title} (#{epic.to_reference})")
end
end end
...@@ -65,6 +65,13 @@ RSpec.describe Banzai::Filter::References::EpicReferenceFilter do ...@@ -65,6 +65,13 @@ RSpec.describe Banzai::Filter::References::EpicReferenceFilter do
expect(link.attr('data-original')).to eq(CGI.escapeHTML(reference)) expect(link.attr('data-original')).to eq(CGI.escapeHTML(reference))
end end
it 'includes a data-reference-format attribute' do
link = doc("&#{epic.iid}+").css('a').first
expect(link).to have_attribute('data-reference-format')
expect(link.attr('data-reference-format')).to eq('+')
end
it 'ignores invalid epic IIDs' do it 'ignores invalid epic IIDs' do
text = "Check &#{non_existing_record_iid}" text = "Check &#{non_existing_record_iid}"
......
...@@ -2,16 +2,18 @@ ...@@ -2,16 +2,18 @@
module Banzai module Banzai
module Filter module Filter
# HTML filter that appends state information to issuable links. # HTML filter that appends extra information to issuable links.
# Runs as a post-process filter as issuable state might change while # Runs as a post-process filter as issuable might change while
# Markdown is in the cache. # Markdown is in the cache.
# #
# This filter supports cross-project references. # This filter supports cross-project references.
class IssuableStateFilter < HTML::Pipeline::Filter class IssuableReferenceExpansionFilter < HTML::Pipeline::Filter
include Gitlab::Utils::StrongMemoize
VISIBLE_STATES = %w(closed merged).freeze VISIBLE_STATES = %w(closed merged).freeze
def call def call
return doc unless context[:issuable_state_filter_enabled] return doc unless context[:issuable_reference_expansion_enabled]
context = RenderContext.new(project, current_user) context = RenderContext.new(project, current_user)
extractor = Banzai::IssuableExtractor.new(context) extractor = Banzai::IssuableExtractor.new(context)
...@@ -19,10 +21,13 @@ module Banzai ...@@ -19,10 +21,13 @@ module Banzai
issuables.each do |node, issuable| issuables.each do |node, issuable|
next if !can_read_cross_project? && cross_referenced?(issuable) next if !can_read_cross_project? && cross_referenced?(issuable)
next unless should_expand?(node, issuable)
if VISIBLE_STATES.include?(issuable.state) && issuable_reference?(node.inner_html, issuable) case node.attr('data-reference-format')
state = moved_issue?(issuable) ? s_("IssuableStatus|moved") : issuable.state when '+'
node.content += " (#{state})" expand_reference_with_title_and_state(node, issuable)
else
expand_reference_with_state(node, issuable)
end end
end end
...@@ -31,12 +36,31 @@ module Banzai ...@@ -31,12 +36,31 @@ module Banzai
private private
# Example: Issue Title (#123 - closed)
def expand_reference_with_title_and_state(node, issuable)
node.content = "#{issuable.title.truncate(50)} (#{node.content}"
node.content += " - #{issuable_state_text(issuable)}" if VISIBLE_STATES.include?(issuable.state)
node.content += ')'
end
# Example: #123 (closed)
def expand_reference_with_state(node, issuable)
node.content += " (#{issuable_state_text(issuable)})"
end
def issuable_state_text(issuable)
moved_issue?(issuable) ? s_("IssuableStatus|moved") : issuable.state
end
def moved_issue?(issuable) def moved_issue?(issuable)
issuable.instance_of?(Issue) && issuable.moved? issuable.instance_of?(Issue) && issuable.moved?
end end
def issuable_reference?(text, issuable) def should_expand?(node, issuable)
CGI.unescapeHTML(text) == issuable.reference_link_text(project || group) # We add this extra check to avoid unescaping HTML and generating reference link text for every reference
return unless node.attr('data-reference-format').present? || VISIBLE_STATES.include?(issuable.state)
CGI.unescapeHTML(node.inner_html) == issuable.reference_link_text(project || group)
end end
def cross_referenced?(issuable) def cross_referenced?(issuable)
...@@ -47,8 +71,10 @@ module Banzai ...@@ -47,8 +71,10 @@ module Banzai
end end
def can_read_cross_project? def can_read_cross_project?
strong_memoize(:can_read_cross_project) do
Ability.allowed?(current_user, :read_cross_project) Ability.allowed?(current_user, :read_cross_project)
end end
end
def current_user def current_user
context[:current_user] context[:current_user]
......
...@@ -205,6 +205,8 @@ module Banzai ...@@ -205,6 +205,8 @@ module Banzai
data_attributes = data_attributes_for(link_content || match, parent, object, data_attributes = data_attributes_for(link_content || match, parent, object,
link_content: !!link_content, link_content: !!link_content,
link_reference: link_reference) link_reference: link_reference)
data_attributes[:reference_format] = matches[:format] if matches.names.include?("format")
data = data_attribute(data_attributes) data = data_attribute(data_attributes)
url = url =
......
...@@ -19,7 +19,7 @@ module Banzai ...@@ -19,7 +19,7 @@ module Banzai
# prevent unnecessary Gitaly calls from being made. # prevent unnecessary Gitaly calls from being made.
Filter::UploadLinkFilter, Filter::UploadLinkFilter,
Filter::RepositoryLinkFilter, Filter::RepositoryLinkFilter,
Filter::IssuableStateFilter, Filter::IssuableReferenceExpansionFilter,
Filter::SuggestionFilter Filter::SuggestionFilter
] ]
end end
......
...@@ -413,7 +413,7 @@ module Gitlab ...@@ -413,7 +413,7 @@ module Gitlab
end end
def issue def issue
@issue ||= /(?<issue>\d+\b)/ @issue ||= /(?<issue>\d+)(?<format>\+)?(?=\W|\z)/
end end
def base64_regex def base64_regex
......
...@@ -321,7 +321,7 @@ RSpec.describe MarkupHelper do ...@@ -321,7 +321,7 @@ RSpec.describe MarkupHelper do
let(:context) do let(:context) do
{ {
pipeline: :wiki, project: project, wiki: wiki, pipeline: :wiki, project: project, wiki: wiki,
page_slug: 'nested/page', issuable_state_filter_enabled: true, page_slug: 'nested/page', issuable_reference_expansion_enabled: true,
repository: wiki_repository repository: wiki_repository
} }
end end
......
...@@ -2,28 +2,27 @@ ...@@ -2,28 +2,27 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe Banzai::Filter::IssuableStateFilter do RSpec.describe Banzai::Filter::IssuableReferenceExpansionFilter do
include ActionView::Helpers::UrlHelper
include FilterSpecHelper include FilterSpecHelper
let(:user) { create(:user) } let_it_be(:user) { create(:user) }
let(:context) { { current_user: user, issuable_state_filter_enabled: true } } let_it_be(:project) { create(:project, :public) }
let(:closed_issue) { create_issue(:closed) } let_it_be(:group) { create(:group) }
let(:project) { create(:project, :public) } let_it_be(:other_project) { create(:project, :public) }
let(:group) { create(:group) } let_it_be(:closed_issue) { create_issue(:closed) }
let(:other_project) { create(:project, :public) }
let(:context) { { current_user: user, issuable_reference_expansion_enabled: true } }
def create_link(text, data) def create_link(text, data)
link_to(text, '', class: 'gfm has-tooltip', data: data) ActionController::Base.helpers.link_to(text, '', class: 'gfm has-tooltip', data: data)
end end
def create_issue(state) def create_issue(state, attributes = {})
create(:issue, state, project: project) create(:issue, state, attributes.merge(project: project))
end end
def create_merge_request(state) def create_merge_request(state, attributes = {})
create(:merge_request, state, create(:merge_request, state, attributes.merge(source_project: project, target_project: project))
source_project: project, target_project: project)
end end
it 'ignores non-GFM links' do it 'ignores non-GFM links' do
...@@ -139,6 +138,30 @@ RSpec.describe Banzai::Filter::IssuableStateFilter do ...@@ -139,6 +138,30 @@ RSpec.describe Banzai::Filter::IssuableStateFilter do
expect(doc.css('a').last.text).to eq("#{moved_issue.to_reference} (moved)") expect(doc.css('a').last.text).to eq("#{moved_issue.to_reference} (moved)")
end end
it 'shows title for references with +' do
issue = create_issue(:opened, title: 'Some issue')
link = create_link(issue.to_reference, issue: issue.id, reference_type: 'issue', reference_format: '+')
doc = filter(link, context)
expect(doc.css('a').last.text).to eq("#{issue.title} (#{issue.to_reference})")
end
it 'truncates long title for references with +' do
issue = create_issue(:opened, title: 'Some issue ' * 10)
link = create_link(issue.to_reference, issue: issue.id, reference_type: 'issue', reference_format: '+')
doc = filter(link, context)
expect(doc.css('a').last.text).to eq("#{issue.title.truncate(50)} (#{issue.to_reference})")
end
it 'shows both title and state for closed references with +' do
issue = create_issue(:closed, title: 'Some issue')
link = create_link(issue.to_reference, issue: issue.id, reference_type: 'issue', reference_format: '+')
doc = filter(link, context)
expect(doc.css('a').last.text).to eq("#{issue.title} (#{issue.to_reference} - closed)")
end
end end
context 'for merge request references' do context 'for merge request references' do
...@@ -197,5 +220,20 @@ RSpec.describe Banzai::Filter::IssuableStateFilter do ...@@ -197,5 +220,20 @@ RSpec.describe Banzai::Filter::IssuableStateFilter do
expect(doc.css('a').last.text).to eq("#{merge_request.to_reference} (merged)") expect(doc.css('a').last.text).to eq("#{merge_request.to_reference} (merged)")
end end
it 'shows title for references with +' do
merge_request = create_merge_request(:opened, title: 'Some merge request')
link = create_link(
merge_request.to_reference,
merge_request: merge_request.id,
reference_type: 'merge_request',
reference_format: '+'
)
doc = filter(link, context)
expect(doc.css('a').last.text).to eq("#{merge_request.title} (#{merge_request.to_reference})")
end
end end
end end
...@@ -116,6 +116,14 @@ RSpec.describe Banzai::Filter::References::IssueReferenceFilter do ...@@ -116,6 +116,14 @@ RSpec.describe Banzai::Filter::References::IssueReferenceFilter do
expect(doc.children.first.attr('data-original')).to eq inner_html expect(doc.children.first.attr('data-original')).to eq inner_html
end end
it 'includes a data-reference-format attribute' do
doc = reference_filter("Issue #{reference}+")
link = doc.css('a').first
expect(link).to have_attribute('data-reference-format')
expect(link.attr('data-reference-format')).to eq('+')
end
it 'supports an :only_path context' do it 'supports an :only_path context' do
doc = reference_filter("Issue #{reference}", only_path: true) doc = reference_filter("Issue #{reference}", only_path: true)
link = doc.css('a').first.attr('href') link = doc.css('a').first.attr('href')
......
...@@ -109,6 +109,14 @@ RSpec.describe Banzai::Filter::References::MergeRequestReferenceFilter do ...@@ -109,6 +109,14 @@ RSpec.describe Banzai::Filter::References::MergeRequestReferenceFilter do
expect(link.attr('data-merge-request')).to eq merge.id.to_s expect(link.attr('data-merge-request')).to eq merge.id.to_s
end end
it 'includes a data-reference-format attribute' do
doc = reference_filter("Merge #{reference}+")
link = doc.css('a').first
expect(link).to have_attribute('data-reference-format')
expect(link.attr('data-reference-format')).to eq('+')
end
it 'supports an :only_path context' do it 'supports an :only_path context' do
doc = reference_filter("Merge #{reference}", only_path: true) doc = reference_filter("Merge #{reference}", only_path: true)
link = doc.css('a').first.attr('href') link = doc.css('a').first.attr('href')
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment