Skip to content

Commit

Permalink
Merge pull request #15992 from opf/feature/51672-starring-favorite-pr…
Browse files Browse the repository at this point in the history
…oject-lists

Feature/51672 starring favorite project lists
  • Loading branch information
toy authored Jul 3, 2024
2 parents b917774 + 9026f0a commit 5cd700b
Show file tree
Hide file tree
Showing 14 changed files with 214 additions and 24 deletions.
3 changes: 3 additions & 0 deletions app/components/open_project/common/submenu_component.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,9 @@
<% selected = child_item.selected ? 'selected' : '' %>
<a class="op-sidemenu--item-action <%= selected %>" href="<%= child_item.href %>" data-test-selector="op-sidemenu--item-action">
<span class="op-sidemenu--item-title"><%= child_item.title %></span>
<% if child_item.favored %>
<%= render Primer::Beta::Octicon.new(icon: "star-fill", "aria-label": I18n.t(:label_favorite), classes: %w[op-sidemenu--item-mark op-primer--star-icon]) %>
<% end %>
</a>
</li>
<% end %>
Expand Down
41 changes: 33 additions & 8 deletions app/components/projects/index_page_header_component.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,39 @@
)
end

header.with_action_menu(menu_arguments: {
anchor_align: :end
},
button_arguments: {
icon: "op-kebab-vertical",
"aria-label": t(:label_more),
data: { "test-selector": "project-more-dropdown-menu" }
}) do |menu|
if can_toggle_favor?
if currently_favored?
header.with_action_icon_button(
icon: "star-fill",
mobile_icon: "star-fill",
label: t(:button_unfavorite),
classes: "op-primer--star-icon",
tag: :a,
href: helpers.build_favorite_path(query, format: :html),
data: { method: :delete, "test-selector": "project-query-unfavorite" },
)
else
header.with_action_icon_button(
icon: "star",
mobile_icon: "star",
label: t(:button_favorite),
tag: :a,
href: helpers.build_favorite_path(query, format: :html),
data: { method: :post, "test-selector": "project-query-favorite" },
)
end
end

header.with_action_menu(
menu_arguments: {
anchor_align: :end
},
button_arguments: {
icon: "op-kebab-vertical",
"aria-label": t(:label_more),
data: { "test-selector": "project-more-dropdown-menu" }
}
) do |menu|
if can_rename?
menu.with_item(
label: t('button_rename'),
Expand Down
4 changes: 4 additions & 0 deletions app/components/projects/index_page_header_component.rb
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,10 @@ def show_state?
state == :show
end

def can_toggle_favor? = query.persisted?

def currently_favored? = query.favored_by?(current_user)

def breadcrumb_items
[
{ href: projects_path, text: t(:label_project_plural) },
Expand Down
13 changes: 12 additions & 1 deletion app/menus/projects/menu.rb
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,10 @@ def selected?(query_params) # rubocop:disable Metrics/AbcSize
end
end

def favored?(query_params)
query_params[:query_id].in?(favored_ids)
end

def query_path(query_params)
projects_path(query_params)
end
Expand Down Expand Up @@ -116,7 +120,14 @@ def shared_filters
end

def persisted_filters
@persisted_filters = ::ProjectQuery.visible(current_user).order(:name)
@persisted_filters ||= ::ProjectQuery
.visible(current_user)
.with_favored_by_user(current_user)
.order(favored: :desc, name: :asc)
end

def favored_ids
@favored_ids ||= persisted_filters.select(&:favored).to_set(&:id)
end

def modification_params?
Expand Down
7 changes: 6 additions & 1 deletion app/menus/submenu.rb
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,8 @@ def query_params(id)
def menu_item(name, query_params)
OpenProject::Menu::MenuItem.new(title: name,
href: query_path(query_params),
selected: selected?(query_params))
selected: selected?(query_params),
favored: favored?(query_params))
end

def selected?(query_params)
Expand All @@ -109,6 +110,10 @@ def selected?(query_params)
true
end

def favored?(_query_params)
false
end

def query_path(query_params)
raise NotImplementedError
end
Expand Down
2 changes: 2 additions & 0 deletions app/models/project_query.rb
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ class ProjectQuery < ApplicationRecord

belongs_to :user

acts_as_favorable

serialize :filters, coder: Queries::Serialization::Filters.new(self)
serialize :orders, coder: Queries::Serialization::Orders.new(self)
serialize :selects, coder: Queries::Serialization::Selects.new(self)
Expand Down
1 change: 1 addition & 0 deletions config/initializers/acts_as_favorable.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
Rails.application.config.to_prepare do
OpenProject::Acts::Favorable::Registry.add(
Project,
ProjectQuery,
reset: true
)
end
7 changes: 4 additions & 3 deletions frontend/src/global_styles/content/_sidemenu.sass
Original file line number Diff line number Diff line change
Expand Up @@ -46,15 +46,16 @@
padding-left: 12px

&--item-title
display: flex
margin-right: auto
display: inline-block
overflow: hidden
text-overflow: ellipsis
white-space: nowrap
line-height: 30px
text-decoration: none
width: 100%

&--item-icon
font-size: 24px
margin-right: 8px

&--item-mark
margin-left: 8px
6 changes: 5 additions & 1 deletion lib/open_project/menu.rb
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@
module OpenProject
module Menu
MenuGroup = Data.define(:header, :children)
MenuItem = Data.define(:title, :href, :selected)
MenuItem = Data.define(:title, :href, :selected, :favored) do
def initialize(title:, href:, selected:, favored: false)
super
end
end
end
end
17 changes: 16 additions & 1 deletion lib_static/open_project/acts/favorable.rb
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ module ClassMethods
#
# acts_as_favorable expects that the including module defines a +visible?(user)+ method,
# as it's used to identify whether a user can actually favorite the object.
def acts_as_favorable
def acts_as_favorable # rubocop:disable Metrics/AbcSize
return if included_modules.include?(InstanceMethods)

class_eval do
Expand All @@ -58,6 +58,21 @@ def acts_as_favorable
includes(:favorites)
.where(favorites: { user_id: })
}

scope :with_favored_by_user, ->(user) {
favorite = ::Favorite.arel_table

join = arel_table
.join(favorite, Arel::Nodes::OuterJoin)
.on(
favorite[:favored_type].eq(base_class.name),
favorite[:favored_id].eq(arel_table[:id]),
favorite[:user_id].eq(user.id)
)
.join_sources

select(arel_table[Arel.star], "(favorites.id IS NOT NULL) AS favored").joins(join)
}
end

Registry.add(self)
Expand Down
15 changes: 14 additions & 1 deletion spec/features/projects/persisted_lists_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@
projects_page.visit!
end

describe 'with the "Active projects" filter' do
context 'with the "Active projects" filter' do
before do
projects_page.set_sidebar_filter "Active projects"
end
Expand Down Expand Up @@ -385,6 +385,19 @@
projects_page.expect_columns("Name", "Status")
projects_page.expect_no_columns("Public")
end

it "allows favoring persisted query" do
projects_page.expect_sidebar_filter("Persisted query", favored: false)

projects_page.set_sidebar_filter("Persisted query")
projects_page.expect_sidebar_filter("Persisted query", selected: true, favored: false)

projects_page.mark_query_favorite
projects_page.expect_sidebar_filter("Persisted query", selected: true, favored: true)

projects_page.unmark_query_favorite
projects_page.expect_sidebar_filter("Persisted query", selected: true, favored: false)
end
end

describe "persisted query access" do
Expand Down
95 changes: 92 additions & 3 deletions spec/menus/projects/menu_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,12 @@
ProjectQuery.create!(name: "Public query", user: build(:user), public: true)
end

shared_let(:view_project_query_role) { create(:view_project_query_role) }

shared_let(:shared_query) do
query = ProjectQuery.create!(name: "Shared query", user: build(:user))
create(:project_query_member, entity: query, user: current_user, roles: [create(:view_project_query_role)])
query
ProjectQuery.create!(name: "Shared query", user: build(:user)).tap do |query|
create(:project_query_member, entity: query, user: current_user, roles: [view_project_query_role])
end
end

subject(:menu_items) { instance.menu_items }
Expand Down Expand Up @@ -106,6 +108,93 @@
end
end

describe "queries order" do
subject(:titles) { menu_items.map { _1.children.map(&:title) } }

shared_let(:another_current_user_query) do
ProjectQuery.create!(name: "Another current user query", user: current_user)
end

shared_let(:another_public_query) do
ProjectQuery.create!(name: "Another public query", user: build(:user), public: true)
end

shared_let(:another_shared_query) do
ProjectQuery.create!(name: "Another shared query", user: build(:user)).tap do |query|
create(:project_query_member, entity: query, user: current_user, roles: [view_project_query_role])
end
end

before do
favored_queries.each do |query|
query.add_favoring_user(current_user)
end
end

context "when no queries are favored" do
let(:favored_queries) { [] }

it "orders persisted titles alphabetically" do
expect(titles).to eq(
[
["Active projects", "My projects", "Favorite projects"],
["Another public query", "Public query"],
["Another current user query", "Current user query"],
["Another shared query", "Shared query"],
["On track", "Off track", "At risk"]
]
)
end
end

context "when some queries are favored" do
let(:favored_queries) do
[
current_user_query,
public_query,
shared_query
]
end

it "orders persisted titles by favor then alphabetically" do
expect(titles).to eq(
[
["Active projects", "My projects", "Favorite projects"],
["Public query", "Another public query"],
["Current user query", "Another current user query"],
["Shared query", "Another shared query"],
["On track", "Off track", "At risk"]
]
)
end
end

context "when all queries are favored" do
let(:favored_queries) do
[
current_user_query,
another_current_user_query,
public_query,
another_public_query,
shared_query,
another_shared_query
]
end

it "orders persisted titles alphabetically" do
expect(titles).to eq(
[
["Active projects", "My projects", "Favorite projects"],
["Another public query", "Public query"],
["Another current user query", "Current user query"],
["Another shared query", "Shared query"],
["On track", "Off track", "At risk"]
]
)
end
end
end

describe "selected children items" do
subject(:selected_menu_items) { menu_items.flat_map(&:children).select(&:selected) }

Expand Down
15 changes: 12 additions & 3 deletions spec/support/components/common/submenu.rb
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,21 @@ class Submenu
include Capybara::RSpecMatchers
include RSpec::Matchers

def expect_item(name, selected: false, visible: true)
def expect_item(name, selected: false, favored: nil, visible: true)
within "#main-menu" do
selected_specifier = selected ? ".selected" : ":not(.selected)"

expect(page).to have_css(".op-sidemenu--item-action#{selected_specifier}", text: name, visible:)
# expect(page).to have_css("[data-test-selector='op-sidemenu--item-action']#{selected_specifier}", text: name, visible:)
if favored.nil?
expect(page).to have_css(".op-sidemenu--item-action#{selected_specifier}", text: name, visible:)
else
item = page.find(".op-sidemenu--item-action#{selected_specifier}", text: name, visible:)

if favored
expect(item).to have_css(".op-primer--star-icon")
else
expect(item).to have_no_css(".op-primer--star-icon")
end
end
end
end

Expand Down
12 changes: 10 additions & 2 deletions spec/support/pages/projects/index.rb
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,8 @@ def expect_title(name)
expect(page).to have_css('[data-test-selector="project-query-name"]', text: name)
end

def expect_sidebar_filter(filter_name, selected: false, visible: true)
submenu.expect_item(filter_name, selected:, visible:)
def expect_sidebar_filter(filter_name, selected: false, favored: false, visible: true)
submenu.expect_item(filter_name, selected:, favored:, visible:)
end

def expect_no_sidebar_filter(filter_name)
Expand Down Expand Up @@ -303,6 +303,14 @@ def set_columns(*columns)
end
end

def mark_query_favorite
page.find('[data-test-selector="project-query-favorite"]').click
end

def unmark_query_favorite
page.find('[data-test-selector="project-query-unfavorite"]').click
end

def click_more_menu_item(item)
wait_for_network_idle if using_cuprite?
page.find('[data-test-selector="project-more-dropdown-menu"]').click
Expand Down

0 comments on commit 5cd700b

Please sign in to comment.