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
return unless current_user
resources_results = [
recent_merge_requests_autocomplete(term),
recent_issues_autocomplete(term),
recent_items_autocomplete(term),
groups_autocomplete(term),
projects_autocomplete(term)
].flatten
......@@ -27,6 +26,10 @@ module SearchHelper
end
end
def recent_items_autocomplete(term)
recent_merge_requests_autocomplete(term) + recent_issues_autocomplete(term)
end
def search_entries_info(collection, scope, term)
return if collection.to_a.empty?
......
......@@ -209,7 +209,8 @@ You can also type in this search bar to see autocomplete suggestions for:
- Project feature pages (try and type **milestones**)
- 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 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
......
......@@ -15,6 +15,7 @@ class Groups::EpicsController < Groups::ApplicationController
before_action :authorize_update_issuable!, only: :update
before_action :authorize_create_epic!, only: [:create, :new]
before_action :verify_group_bulk_edit_enabled!, only: [:bulk_update]
after_action :log_epic_show, only: :show
before_action do
push_frontend_feature_flag(:vue_issuable_epic_sidebar, @group)
......@@ -113,6 +114,12 @@ class Groups::EpicsController < Groups::ApplicationController
@preload_for_collection ||= [:group, :author, :labels]
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!
return render_404 unless can?(current_user, :create_epic, group)
end
......
......@@ -19,6 +19,11 @@ module EE
options
end
override :recent_items_autocomplete
def recent_items_autocomplete(term)
super + recent_epics_autocomplete(term)
end
override :search_blob_title
def search_blob_title(project, path)
if @project
......@@ -72,6 +77,20 @@ module EE
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)
context = @project.presence || @group.presence || :dashboard
......
......@@ -17,6 +17,7 @@ module EE
include FromUnion
include EpicTreeSorting
include Presentable
include IdInOrdered
enum state_id: {
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
expect(response).to render_template 'groups/epics/show'
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
it 'returns a not found 404 response' do
show_epic
......
......@@ -51,6 +51,47 @@ RSpec.describe SearchHelper do
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
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
expect(result.keys).to match_array(%i[category id label url avatar_url])
end
it 'includes the users recent issues' do
it 'includes the users recently viewed issues' do
recent_issues = instance_double(::Gitlab::Search::RecentIssues)
expect(::Gitlab::Search::RecentIssues).to receive(:new).with(user: user).and_return(recent_issues)
project1 = create(:project, :with_avatar, namespace: user.namespace)
......@@ -104,7 +104,7 @@ RSpec.describe SearchHelper do
})
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)
expect(::Gitlab::Search::RecentMergeRequests).to receive(:new).with(user: user).and_return(recent_merge_requests)
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