Commit e249fdfe authored by Dylan Griffith's avatar Dylan Griffith

Add search autocomplete suggestions for recently viewed epics

This implements the same thing we implemented for [issues](
https://gitlab.com/gitlab-org/gitlab/-/merge_requests/40669
) and [merge requests](
https://gitlab.com/gitlab-org/gitlab/-/merge_requests/42560
) now for Epics. This required a slightly different approach as the
`EpicsFinder` was not suitable for this purpose. At present it only
supports searching within a single group. And extending it to support
wider use cases would likely lead to performance issues as there is no
equivalent group permissions cache like `project_authorizations` table
to efficiently determine which groups a user can view epics in.

The simpler thing to do here was to just manually check the permissions
for each returned epic. This leaves a very edge case scenarion in which
a user was previously able to see an epic and looked at that epic in
their last 100 viewed epics and then performs a search and now only sees
4 suggestions (instead of the expected 5). This in theory, if the epic
was renamed and the new name contained something important the user
shouldn't see, could lead to the leak of an existence of that name in
the search results. Considering this edge case is so unlikely it seems
safe to not worry too much about it.
parent 1fc824a9
...@@ -7,8 +7,7 @@ module SearchHelper ...@@ -7,8 +7,7 @@ module SearchHelper
return unless current_user return unless current_user
resources_results = [ resources_results = [
recent_merge_requests_autocomplete(term), recent_items_autocomplete(term),
recent_issues_autocomplete(term),
groups_autocomplete(term), groups_autocomplete(term),
projects_autocomplete(term) projects_autocomplete(term)
].flatten ].flatten
...@@ -27,6 +26,10 @@ module SearchHelper ...@@ -27,6 +26,10 @@ module SearchHelper
end end
end end
def recent_items_autocomplete(term)
recent_merge_requests_autocomplete(term) + recent_issues_autocomplete(term)
end
def search_entries_info(collection, scope, term) def search_entries_info(collection, scope, term)
return if collection.to_a.empty? return if collection.to_a.empty?
......
...@@ -209,7 +209,8 @@ You can also type in this search bar to see autocomplete suggestions for: ...@@ -209,7 +209,8 @@ You can also type in this search bar to see autocomplete suggestions for:
- Project feature pages (try and type **milestones**) - Project feature pages (try and type **milestones**)
- Various settings pages (try and type **user settings**) - Various settings pages (try and type **user settings**)
- Recently viewed issues (try and type some word from the title of a recently viewed issue) - Recently viewed issues (try and type some word from the title of a recently viewed issue)
- Recently viewed merge requests (try and type some word from the title of a recently merge request) - Recently viewed merge requests (try and type some word from the title of a recently viewed merge request)
- Recently viewed epics (try and type some word from the title of a recently viewed epic)
## Basic search ## Basic search
......
...@@ -15,6 +15,7 @@ class Groups::EpicsController < Groups::ApplicationController ...@@ -15,6 +15,7 @@ class Groups::EpicsController < Groups::ApplicationController
before_action :authorize_update_issuable!, only: :update before_action :authorize_update_issuable!, only: :update
before_action :authorize_create_epic!, only: [:create, :new] before_action :authorize_create_epic!, only: [:create, :new]
before_action :verify_group_bulk_edit_enabled!, only: [:bulk_update] before_action :verify_group_bulk_edit_enabled!, only: [:bulk_update]
after_action :log_epic_show, only: :show
before_action do before_action do
push_frontend_feature_flag(:vue_issuable_epic_sidebar, @group) push_frontend_feature_flag(:vue_issuable_epic_sidebar, @group)
...@@ -113,6 +114,12 @@ class Groups::EpicsController < Groups::ApplicationController ...@@ -113,6 +114,12 @@ class Groups::EpicsController < Groups::ApplicationController
@preload_for_collection ||= [:group, :author, :labels] @preload_for_collection ||= [:group, :author, :labels]
end end
def log_epic_show
return unless current_user && @epic
::Gitlab::Search::RecentEpics.new(user: current_user).log_view(@epic)
end
def authorize_create_epic! def authorize_create_epic!
return render_404 unless can?(current_user, :create_epic, group) return render_404 unless can?(current_user, :create_epic, group)
end end
......
...@@ -19,6 +19,11 @@ module EE ...@@ -19,6 +19,11 @@ module EE
options options
end end
override :recent_items_autocomplete
def recent_items_autocomplete(term)
super + recent_epics_autocomplete(term)
end
override :search_blob_title override :search_blob_title
def search_blob_title(project, path) def search_blob_title(project, path)
if @project if @project
...@@ -72,6 +77,20 @@ module EE ...@@ -72,6 +77,20 @@ module EE
private private
def recent_epics_autocomplete(term)
return [] unless current_user
::Gitlab::Search::RecentEpics.new(user: current_user).search(term).map do |e|
{
category: "Recent epics",
id: e.id,
label: search_result_sanitize(e.title),
url: epic_path(e),
avatar_url: e.group.avatar_url || ''
}
end
end
def search_multiple_assignees?(type) def search_multiple_assignees?(type)
context = @project.presence || @group.presence || :dashboard context = @project.presence || @group.presence || :dashboard
......
...@@ -17,6 +17,7 @@ module EE ...@@ -17,6 +17,7 @@ module EE
include FromUnion include FromUnion
include EpicTreeSorting include EpicTreeSorting
include Presentable include Presentable
include IdInOrdered
enum state_id: { enum state_id: {
opened: ::Epic.available_states[:opened], opened: ::Epic.available_states[:opened],
......
---
title: Add search autocomplete suggestions for recently viewed epics
merge_request: 43964
author:
type: added
# frozen_string_literal: true
module Gitlab
module Search
class RecentEpics < RecentItems
extend ::Gitlab::Utils::Override
override :search
# rubocop: disable CodeReuse/ActiveRecord
def search(term)
epics = Epic.full_search(term, matched_columns: 'title')
.id_in_ordered(latest_ids).limit(::Gitlab::Search::RecentItems::SEARCH_LIMIT)
# Since EpicsFinder does not support searching globally (ie. applying
# global permissions) the most efficient option is just to load the
# last 5 matching recently viewed epics and then do an explicit
# permissions check
disallowed = epics.reject { |epic| Ability.allowed?(user, :read_epic, epic) }
return epics if disallowed.empty?
epics.where.not(id: disallowed.map(&:id))
end
# rubocop: enable CodeReuse/ActiveRecord
private
def type
Epic
end
end
end
end
...@@ -285,6 +285,18 @@ RSpec.describe Groups::EpicsController do ...@@ -285,6 +285,18 @@ RSpec.describe Groups::EpicsController do
expect(response).to render_template 'groups/epics/show' expect(response).to render_template 'groups/epics/show'
end end
it 'logs the view with Gitlab::Search::RecentEpics' do
group.add_developer(user)
recent_epics_double = instance_double(::Gitlab::Search::RecentEpics, log_view: nil)
expect(::Gitlab::Search::RecentEpics).to receive(:new).with(user: user).and_return(recent_epics_double)
show_epic
expect(response).to be_successful
expect(recent_epics_double).to have_received(:log_view).with(epic)
end
context 'with unauthorized user' do context 'with unauthorized user' do
it 'returns a not found 404 response' do it 'returns a not found 404 response' do
show_epic show_epic
......
...@@ -51,6 +51,47 @@ RSpec.describe SearchHelper do ...@@ -51,6 +51,47 @@ RSpec.describe SearchHelper do
end end
end end
describe 'search_autocomplete_opts' do
context "with a user" do
let(:user) { create(:user) }
before do
allow(self).to receive(:current_user).and_return(user)
end
it 'includes the users recently viewed epics' do
recent_epics = instance_double(::Gitlab::Search::RecentEpics)
expect(::Gitlab::Search::RecentEpics).to receive(:new).with(user: user).and_return(recent_epics)
group1 = create(:group, :public, :with_avatar)
group2 = create(:group, :public)
epic1 = create(:epic, title: 'epic 1', group: group1)
epic2 = create(:epic, title: 'epic 2', group: group2)
expect(recent_epics).to receive(:search).with('the search term').and_return(Epic.id_in_ordered([epic1.id, epic2.id]))
results = search_autocomplete_opts("the search term")
expect(results.count).to eq(2)
expect(results[0]).to include({
category: 'Recent epics',
id: epic1.id,
label: 'epic 1',
url: Gitlab::Routing.url_helpers.group_epic_path(epic1.group, epic1),
avatar_url: group1.avatar_url
})
expect(results[1]).to include({
category: 'Recent epics',
id: epic2.id,
label: 'epic 2',
url: Gitlab::Routing.url_helpers.group_epic_path(epic2.group, epic2),
avatar_url: '' # This group didn't have an avatar so set this to ''
})
end
end
end
describe '#project_autocomplete' do describe '#project_autocomplete' do
let(:user) { create(:user) } let(:user) { create(:user) }
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe ::Gitlab::Search::RecentEpics do
let(:parent_type) { :group }
def create_item(content:, parent:)
create(:epic, title: content, group: parent)
end
before do
stub_licensed_features(epics: true)
end
it_behaves_like 'search recent items'
end
...@@ -73,7 +73,7 @@ RSpec.describe SearchHelper do ...@@ -73,7 +73,7 @@ RSpec.describe SearchHelper do
expect(result.keys).to match_array(%i[category id label url avatar_url]) expect(result.keys).to match_array(%i[category id label url avatar_url])
end end
it 'includes the users recent issues' do it 'includes the users recently viewed issues' do
recent_issues = instance_double(::Gitlab::Search::RecentIssues) recent_issues = instance_double(::Gitlab::Search::RecentIssues)
expect(::Gitlab::Search::RecentIssues).to receive(:new).with(user: user).and_return(recent_issues) expect(::Gitlab::Search::RecentIssues).to receive(:new).with(user: user).and_return(recent_issues)
project1 = create(:project, :with_avatar, namespace: user.namespace) project1 = create(:project, :with_avatar, namespace: user.namespace)
...@@ -104,7 +104,7 @@ RSpec.describe SearchHelper do ...@@ -104,7 +104,7 @@ RSpec.describe SearchHelper do
}) })
end end
it 'includes the users recent merge requests' do it 'includes the users recently viewed merge requests' do
recent_merge_requests = instance_double(::Gitlab::Search::RecentMergeRequests) recent_merge_requests = instance_double(::Gitlab::Search::RecentMergeRequests)
expect(::Gitlab::Search::RecentMergeRequests).to receive(:new).with(user: user).and_return(recent_merge_requests) expect(::Gitlab::Search::RecentMergeRequests).to receive(:new).with(user: user).and_return(recent_merge_requests)
project1 = create(:project, :with_avatar, namespace: user.namespace) project1 = create(:project, :with_avatar, namespace: user.namespace)
......
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