mirror of
https://github.com/opf/openproject.git
synced 2026-06-13 19:20:00 +00:00
Fix wording: Change "Favored" to "Favorited"
Favorite is the correct term in the context of expressing a preference
for a particular project / other OpenProject domain object.
Updates `ActsAsFavorable` to `ActsAsFavoritable`, as well as filenames,
identifiers and strings to:
favored => favorited
favorable => favoritable
favoring => favoriting
This commit is contained in:
@@ -84,7 +84,7 @@
|
||||
<%= render Primer::Beta::Octicon.new(icon: "op-enterprise-addons", "aria-label": I18n.t(:label_enterprise_edition), classes: "upsell-colored", ml: 2) %>
|
||||
<% end %>
|
||||
|
||||
<% if child_item.favored %>
|
||||
<% if child_item.favorited %>
|
||||
<%= render Primer::Beta::Octicon.new(icon: "star-fill", "aria-label": I18n.t(:label_favorite), classes: %w[op-submenu--item-mark op-primer--star-icon], ml: 2) %>
|
||||
<% end %>
|
||||
</span>
|
||||
|
||||
@@ -45,8 +45,8 @@
|
||||
)
|
||||
end
|
||||
|
||||
if can_toggle_favor?
|
||||
if currently_favored?
|
||||
if can_toggle_favorite?
|
||||
if currently_favorited?
|
||||
header.with_action_icon_button(
|
||||
icon: "star-fill",
|
||||
mobile_icon: "star-fill",
|
||||
|
||||
@@ -106,9 +106,9 @@ class Projects::IndexPageHeaderComponent < ApplicationComponent
|
||||
query.persisted?
|
||||
end
|
||||
|
||||
def can_toggle_favor? = query.persisted?
|
||||
def can_toggle_favorite? = query.persisted?
|
||||
|
||||
def currently_favored? = query.favored_by?(current_user)
|
||||
def currently_favorited? = query.favorited_by?(current_user)
|
||||
|
||||
def breadcrumb_items
|
||||
[
|
||||
|
||||
@@ -48,7 +48,7 @@ class Projects::ProjectsFiltersComponent < Filter::FilterComponent
|
||||
Queries::Filters::Shared::CustomFields::Base,
|
||||
Queries::Projects::Filters::ActiveFilter,
|
||||
Queries::Projects::Filters::CreatedAtFilter,
|
||||
Queries::Projects::Filters::FavoredFilter,
|
||||
Queries::Projects::Filters::FavoritedFilter,
|
||||
Queries::Projects::Filters::IdFilter,
|
||||
Queries::Projects::Filters::LatestActivityAtFilter,
|
||||
Queries::Projects::Filters::ProjectPhaseAnyFilter,
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
module Projects
|
||||
class RowComponent < ::RowComponent
|
||||
delegate :identifier, to: :project
|
||||
delegate :favored_project_ids,
|
||||
delegate :favorited_project_ids,
|
||||
:project_phase_by_definition,
|
||||
to: :table
|
||||
|
||||
@@ -47,25 +47,25 @@ module Projects
|
||||
""
|
||||
end
|
||||
|
||||
def favored
|
||||
def favorited
|
||||
render(Primer::Beta::IconButton.new(
|
||||
icon: currently_favored? ? "star-fill" : "star",
|
||||
icon: currently_favorited? ? "star-fill" : "star",
|
||||
scheme: :invisible,
|
||||
mobile_icon: currently_favored? ? "star-fill" : "star",
|
||||
mobile_icon: currently_favorited? ? "star-fill" : "star",
|
||||
size: :medium,
|
||||
tag: :a,
|
||||
tooltip_direction: :e,
|
||||
href: helpers.build_favorite_path(project, format: :html),
|
||||
data: { "turbo-method": currently_favored? ? :delete : :post },
|
||||
classes: currently_favored? ? "op-primer--star-icon " : "op-project-row-component--favorite",
|
||||
label: currently_favored? ? I18n.t(:button_unfavorite) : I18n.t(:button_favorite),
|
||||
aria: { label: currently_favored? ? I18n.t(:button_unfavorite) : I18n.t(:button_favorite) },
|
||||
data: { "turbo-method": currently_favorited? ? :delete : :post },
|
||||
classes: currently_favorited? ? "op-primer--star-icon " : "op-project-row-component--favorite",
|
||||
label: currently_favorited? ? I18n.t(:button_unfavorite) : I18n.t(:button_favorite),
|
||||
aria: { label: currently_favorited? ? I18n.t(:button_unfavorite) : I18n.t(:button_favorite) },
|
||||
test_selector: "project-list-favorite-button"
|
||||
))
|
||||
end
|
||||
|
||||
def currently_favored?
|
||||
@currently_favored ||= favored_project_ids.include?(project.id)
|
||||
def currently_favorited?
|
||||
@currently_favorited ||= favorited_project_ids.include?(project.id)
|
||||
end
|
||||
|
||||
def column_value(column)
|
||||
@@ -215,10 +215,10 @@ module Projects
|
||||
|
||||
def additional_css_class(column)
|
||||
if column.attribute == :name
|
||||
"project--hierarchy #{project.archived? ? 'archived' : ''}"
|
||||
"project--hierarchy #{'archived' if project.archived?}"
|
||||
elsif %i[status_explanation description].include?(column.attribute)
|
||||
"project-long-text-container"
|
||||
elsif column.attribute == :favored
|
||||
elsif column.attribute == :favorited
|
||||
"-w-abs-45"
|
||||
elsif custom_field_column?(column)
|
||||
cf = column.custom_field
|
||||
@@ -267,7 +267,7 @@ module Projects
|
||||
end
|
||||
|
||||
def more_menu_favorite_item
|
||||
return if currently_favored?
|
||||
return if currently_favorited?
|
||||
|
||||
{
|
||||
scheme: :default,
|
||||
@@ -280,7 +280,7 @@ module Projects
|
||||
end
|
||||
|
||||
def more_menu_unfavorite_item
|
||||
return unless currently_favored?
|
||||
return unless currently_favorited?
|
||||
|
||||
{
|
||||
scheme: :default,
|
||||
|
||||
@@ -191,8 +191,8 @@ module Projects
|
||||
end
|
||||
end
|
||||
|
||||
def favored_project_ids
|
||||
@favored_project_ids ||= Favorite.where(user: current_user, favored_type: "Project").pluck(:favored_id)
|
||||
def favorited_project_ids
|
||||
@favorited_project_ids ||= Favorite.where(user: current_user, favorited_type: "Project").pluck(:favorited_id)
|
||||
end
|
||||
|
||||
def project_phase_by_definition(definition, project)
|
||||
|
||||
@@ -29,33 +29,33 @@
|
||||
#++
|
||||
|
||||
class FavoritesController < ApplicationController
|
||||
before_action :find_favored_by_object
|
||||
before_action :find_favorited_by_object
|
||||
before_action :require_login
|
||||
no_authorization_required! :favorite, :unfavorite
|
||||
|
||||
def favorite
|
||||
if @favored.visible?(User.current)
|
||||
set_favored(User.current, true)
|
||||
if @favorited.visible?(User.current)
|
||||
set_favorited(User.current, true)
|
||||
else
|
||||
render_403
|
||||
end
|
||||
end
|
||||
|
||||
def unfavorite
|
||||
set_favored(User.current, false)
|
||||
set_favorited(User.current, false)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def find_favored_by_object
|
||||
def find_favorited_by_object
|
||||
model_name = params[:object_type]
|
||||
klass = ::OpenProject::Acts::Favorable::Registry.instance(model_name)
|
||||
@favored = klass&.find(params[:object_id])
|
||||
render_404 unless @favored
|
||||
klass = ::OpenProject::Acts::Favoritable::Registry.instance(model_name)
|
||||
@favorited = klass&.find(params[:object_id])
|
||||
render_404 unless @favorited
|
||||
end
|
||||
|
||||
def set_favored(user, favored)
|
||||
@favored.set_favored(user, favored:)
|
||||
def set_favorited(user, favorited)
|
||||
@favorited.set_favorited(user, favorited:)
|
||||
|
||||
respond_to do |format|
|
||||
format.html { redirect_back(fallback_location: home_url, status: 303) }
|
||||
|
||||
@@ -37,7 +37,7 @@ class HomescreenController < ApplicationController
|
||||
|
||||
def index
|
||||
@newest_projects = Project.visible.newest.take(3)
|
||||
@favorite_projects = Project.visible.active.favored_by(User.current)
|
||||
@favorite_projects = Project.visible.active.favorited_by(User.current)
|
||||
@newest_users = User.active.newest.take(3)
|
||||
@news = News.latest(count: 3)
|
||||
@announcement = Announcement.active_and_current
|
||||
|
||||
@@ -347,7 +347,7 @@ module SortHelper
|
||||
data = options.delete(:data) || {}
|
||||
|
||||
options[:title] = sort_header_title(column, caption, options) if with_title
|
||||
options[:icon_only_header] = column == :favored
|
||||
options[:icon_only_header] = column == :favorited
|
||||
|
||||
within_sort_header_tag_hierarchy(options, sort_class(column)) do
|
||||
yield(column, caption, default_order, allowed_params:, param:, lang:, title: options[:title],
|
||||
@@ -401,7 +401,7 @@ module SortHelper
|
||||
content_args = html_options.merge(rel: :nofollow, param: nil)
|
||||
|
||||
render Primer::Alpha::ActionMenu.new(menu_id: "menu-#{attribute}") do |menu|
|
||||
action_button(menu, column, caption, favorite: column == :favored)
|
||||
action_button(menu, column, caption, favorite: column == :favorited)
|
||||
|
||||
# Some columns are not sortable or do not offer a suitable filter. Omit those actions for them.
|
||||
sort_actions(menu, attribute, default_order, content_args:, allowed_params:, **html_options) if sortable
|
||||
|
||||
@@ -68,8 +68,8 @@ module Projects
|
||||
end
|
||||
end
|
||||
|
||||
def favored?(query_params)
|
||||
query_params[:query_id].in?(favored_ids)
|
||||
def favorited?(query_params)
|
||||
query_params[:query_id].in?(favorited_ids)
|
||||
end
|
||||
|
||||
def query_path(query_params)
|
||||
@@ -82,7 +82,7 @@ module Projects
|
||||
static_filters [
|
||||
ProjectQueries::Static::ACTIVE,
|
||||
current_user.logged? ? ProjectQueries::Static::MY : nil,
|
||||
current_user.logged? ? ProjectQueries::Static::FAVORED : nil,
|
||||
current_user.logged? ? ProjectQueries::Static::FAVORITED : nil,
|
||||
current_user.admin? ? ProjectQueries::Static::ARCHIVED : nil
|
||||
].compact
|
||||
end
|
||||
@@ -116,12 +116,12 @@ module Projects
|
||||
def persisted_filters
|
||||
@persisted_filters ||= ::ProjectQuery
|
||||
.visible(current_user)
|
||||
.with_favored_by_user(current_user)
|
||||
.order(favored: :desc, name: :asc)
|
||||
.with_favorited_by_user(current_user)
|
||||
.order(favorited: :desc, name: :asc)
|
||||
end
|
||||
|
||||
def favored_ids
|
||||
@favored_ids ||= persisted_filters.select(&:favored).to_set(&:id)
|
||||
def favorited_ids
|
||||
@favorited_ids ||= persisted_filters.select(&:favorited).to_set(&:id)
|
||||
end
|
||||
|
||||
def modification_params?
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
# ++
|
||||
class Submenu
|
||||
include Rails.application.routes.url_helpers
|
||||
|
||||
attr_reader :view_type, :project, :params
|
||||
|
||||
def initialize(view_type:, params:, project: nil)
|
||||
@@ -117,7 +118,7 @@ class Submenu
|
||||
icon: icon_map.fetch(icon_key, icon_key),
|
||||
count:,
|
||||
selected:,
|
||||
favored: favored?(query_params),
|
||||
favorited: favorited?(query_params),
|
||||
show_enterprise_icon:)
|
||||
end
|
||||
|
||||
@@ -129,14 +130,14 @@ class Submenu
|
||||
end
|
||||
end
|
||||
|
||||
if query_params.empty? && (%i[filters query_props query_id name].any? { |k| params.key? k })
|
||||
if query_params.empty? && %i[filters query_props query_id name].any? { |k| params.key? k }
|
||||
return false
|
||||
end
|
||||
|
||||
true
|
||||
end
|
||||
|
||||
def favored?(_query_params)
|
||||
def favorited?(_query_params)
|
||||
false
|
||||
end
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
|
||||
class ApplicationRecord < ActiveRecord::Base
|
||||
include ::OpenProject::Acts::Watchable
|
||||
include ::OpenProject::Acts::Favorable
|
||||
include ::OpenProject::Acts::Favoritable
|
||||
|
||||
self.abstract_class = true
|
||||
|
||||
|
||||
@@ -30,8 +30,8 @@
|
||||
|
||||
class Favorite < ApplicationRecord
|
||||
belongs_to :user
|
||||
belongs_to :favored, polymorphic: true
|
||||
belongs_to :favorited, polymorphic: true
|
||||
|
||||
validates :user, presence: true
|
||||
validates :favored, presence: true
|
||||
validates :favorited, presence: true
|
||||
end
|
||||
|
||||
@@ -108,7 +108,7 @@ class Project < ApplicationRecord
|
||||
store_attribute :settings, :deactivate_work_package_attachments, :boolean
|
||||
store_attribute :settings, :enabled_internal_comments, :boolean
|
||||
|
||||
acts_as_favorable
|
||||
acts_as_favoritable
|
||||
|
||||
acts_as_customizable validate_on: :saving_custom_fields
|
||||
# extended in Projects::CustomFields in order to support sections
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
class ProjectQueries::Static
|
||||
ACTIVE = "active"
|
||||
MY = "my"
|
||||
FAVORED = "favored"
|
||||
FAVORITED = "favorited"
|
||||
ARCHIVED = "archived"
|
||||
ON_TRACK = "on_track"
|
||||
OFF_TRACK = "off_track"
|
||||
@@ -46,8 +46,8 @@ class ProjectQueries::Static
|
||||
static_query_active
|
||||
when MY
|
||||
static_query_my
|
||||
when FAVORED
|
||||
static_query_favored
|
||||
when FAVORITED
|
||||
static_query_favorited
|
||||
when ARCHIVED
|
||||
static_query_archived
|
||||
when ON_TRACK
|
||||
@@ -73,9 +73,9 @@ class ProjectQueries::Static
|
||||
end
|
||||
end
|
||||
|
||||
def static_query_favored
|
||||
list_with(:"projects.lists.favored") do |query|
|
||||
query.where("favored", "=", OpenProject::Database::DB_VALUE_TRUE)
|
||||
def static_query_favorited
|
||||
list_with(:"projects.lists.favorited") do |query|
|
||||
query.where("favorited", "=", OpenProject::Database::DB_VALUE_TRUE)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ class ProjectQuery < ApplicationRecord
|
||||
|
||||
belongs_to :user
|
||||
|
||||
acts_as_favorable
|
||||
acts_as_favoritable
|
||||
|
||||
serialize :filters, coder: Queries::Serialization::Filters.new(self)
|
||||
serialize :orders, coder: Queries::Serialization::Orders.new(self)
|
||||
|
||||
+4
-4
@@ -29,15 +29,15 @@
|
||||
#++
|
||||
module Projects::Exports
|
||||
module Formatters
|
||||
class Favored < ::Exports::Formatters::Default
|
||||
class Favorited < ::Exports::Formatters::Default
|
||||
def self.apply?(attribute, export_format)
|
||||
export_format == :pdf && attribute.to_sym == :favored
|
||||
export_format == :pdf && attribute.to_sym == :favorited
|
||||
end
|
||||
|
||||
##
|
||||
# Takes a project and returns yes/no depending on the favored attribute
|
||||
# Takes a project and returns yes/no depending on the favorited attribute
|
||||
def format(project, **)
|
||||
project.favored_by?(User.current) ? I18n.t(:general_text_Yes) : I18n.t(:general_text_No)
|
||||
project.favorited_by?(User.current) ? I18n.t(:general_text_Yes) : I18n.t(:general_text_No)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -170,7 +170,7 @@ module Projects::Exports::PDFExport
|
||||
end
|
||||
|
||||
def can_view_attribute?(_project, attribute)
|
||||
attribute && %i[name favored].exclude?(attribute)
|
||||
attribute && %i[name favorited].exclude?(attribute)
|
||||
end
|
||||
|
||||
def user_can_view_project_phases?(project)
|
||||
|
||||
@@ -36,7 +36,7 @@ module Queries::Projects
|
||||
filter Filters::AvailableProjectAttributesFilter
|
||||
filter Filters::CreatedAtFilter
|
||||
filter Filters::CustomFieldFilter
|
||||
filter Filters::FavoredFilter
|
||||
filter Filters::FavoritedFilter
|
||||
filter Filters::IdFilter
|
||||
filter Filters::LatestActivityAtFilter
|
||||
filter Filters::ProjectPhaseAnyFilter
|
||||
@@ -67,7 +67,7 @@ module Queries::Projects
|
||||
select Selects::CreatedAt
|
||||
select Selects::CustomField
|
||||
select Selects::Default
|
||||
select Selects::Favored
|
||||
select Selects::Favorited
|
||||
select Selects::LatestActivityAt
|
||||
select Selects::ProjectPhase
|
||||
select Selects::RequiredDiskSpace
|
||||
|
||||
+7
-7
@@ -28,11 +28,11 @@
|
||||
# See COPYRIGHT and LICENSE files for more details.
|
||||
#++
|
||||
|
||||
class Queries::Projects::Filters::FavoredFilter < Queries::Projects::Filters::Base
|
||||
class Queries::Projects::Filters::FavoritedFilter < Queries::Projects::Filters::Base
|
||||
include Queries::Filters::Shared::BooleanFilter
|
||||
|
||||
def self.key
|
||||
:favored
|
||||
:favorited
|
||||
end
|
||||
|
||||
def human_name
|
||||
@@ -46,9 +46,9 @@ class Queries::Projects::Filters::FavoredFilter < Queries::Projects::Filters::Ba
|
||||
def apply_to(_query_scope)
|
||||
if (values.first == OpenProject::Database::DB_VALUE_TRUE && operator_strategy == Queries::Operators::BooleanEquals) ||
|
||||
(values.first == OpenProject::Database::DB_VALUE_FALSE && operator_strategy == Queries::Operators::BooleanNotEquals)
|
||||
super.where(id: favored_project_ids)
|
||||
super.where(id: favorited_project_ids)
|
||||
else
|
||||
super.where.not(id: favored_project_ids)
|
||||
super.where.not(id: favorited_project_ids)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -57,9 +57,9 @@ class Queries::Projects::Filters::FavoredFilter < Queries::Projects::Filters::Ba
|
||||
nil
|
||||
end
|
||||
|
||||
def favored_project_ids
|
||||
def favorited_project_ids
|
||||
Favorite
|
||||
.where(favored_type: "Project", user_id: User.current.id)
|
||||
.select(:favored_id)
|
||||
.where(favorited_type: "Project", user_id: User.current.id)
|
||||
.select(:favorited_id)
|
||||
end
|
||||
end
|
||||
+2
-2
@@ -28,9 +28,9 @@
|
||||
# See COPYRIGHT and LICENSE files for more details.
|
||||
# ++
|
||||
|
||||
class Queries::Projects::Selects::Favored < Queries::Selects::Base
|
||||
class Queries::Projects::Selects::Favorited < Queries::Selects::Base
|
||||
def self.key
|
||||
:favored
|
||||
:favorited
|
||||
end
|
||||
|
||||
def self.available?
|
||||
@@ -94,6 +94,6 @@ class ProjectQueries::SetAttributesService < BaseServices::SetAttributes
|
||||
end
|
||||
|
||||
def default_columns
|
||||
(["favored", "name"] + Setting.enabled_projects_columns).uniq
|
||||
(["favorited", "name"] + Setting.enabled_projects_columns).uniq
|
||||
end
|
||||
end
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
<div class="op-widget-box--body">
|
||||
<% if @favorite_projects.any? %>
|
||||
<p class="widget-box--additional-info"><%= t("projects.lists.favored") %></p>
|
||||
<p class="widget-box--additional-info"><%= t("projects.lists.favorited") %></p>
|
||||
<ul class="widget-box--arrow-links">
|
||||
<% @favorite_projects.each do |project| %>
|
||||
<li>
|
||||
|
||||
@@ -463,7 +463,7 @@ module Settings
|
||||
default: false
|
||||
},
|
||||
enabled_projects_columns: {
|
||||
default: %w[favored name project_status public created_at latest_activity_at required_disk_space],
|
||||
default: %w[favorited name project_status public created_at latest_activity_at required_disk_space],
|
||||
allowed: -> { ProjectQuery.new.available_selects.map { |s| s.attribute.to_s } }
|
||||
},
|
||||
enabled_scm: {
|
||||
|
||||
+2
-2
@@ -30,10 +30,10 @@
|
||||
|
||||
# Be sure to restart your server when you modify this file.
|
||||
|
||||
# For development and non-eager load mode, we need to load models using acts_as_favorable manually
|
||||
# For development and non-eager load mode, we need to load models using acts_as_favoritable manually
|
||||
# as no eager loading takes place
|
||||
Rails.application.config.to_prepare do
|
||||
OpenProject::Acts::Favorable::Registry.add(
|
||||
OpenProject::Acts::Favoritable::Registry.add(
|
||||
Project,
|
||||
ProjectQuery,
|
||||
reset: true
|
||||
@@ -57,7 +57,7 @@ Rails.application.configure do |application|
|
||||
formatter Project, Projects::Exports::Formatters::Description
|
||||
formatter Project, Projects::Exports::Formatters::Public
|
||||
formatter Project, Projects::Exports::Formatters::Active
|
||||
formatter Project, Projects::Exports::Formatters::Favored
|
||||
formatter Project, Projects::Exports::Formatters::Favorited
|
||||
formatter Project, Projects::Exports::Formatters::RequiredDiskSpace
|
||||
end
|
||||
end
|
||||
|
||||
@@ -463,7 +463,7 @@ en:
|
||||
lists:
|
||||
active: "Active projects"
|
||||
my: "My projects"
|
||||
favored: "Favorite projects"
|
||||
favorited: "Favorite projects"
|
||||
archived: "Archived projects"
|
||||
shared: "Shared project lists"
|
||||
my_lists: "My project lists"
|
||||
|
||||
+1
-1
@@ -238,7 +238,7 @@ Rails.application.routes.draw do
|
||||
end
|
||||
|
||||
# generic route for adding/removing favorites
|
||||
scope ":object_type/:object_id", constraints: OpenProject::Acts::Favorable::RouteConstraint do
|
||||
scope ":object_type/:object_id", constraints: OpenProject::Acts::Favoritable::RouteConstraint do
|
||||
post "/favorite" => "favorites#favorite"
|
||||
delete "/favorite" => "favorites#unfavorite"
|
||||
end
|
||||
|
||||
@@ -206,7 +206,7 @@ OPENPROJECT_EMAILS__FOOTER (default={"en" => ""}) Emails footer
|
||||
OPENPROJECT_EMAILS__HEADER (default={"en" => ""}) Emails header
|
||||
OPENPROJECT_EMAILS__SALUTATION (default=:firstname) Address user in emails with
|
||||
OPENPROJECT_ENABLE__INTERNAL__ASSETS__SERVER (default=false) Serve assets through the Rails internal asset server
|
||||
OPENPROJECT_ENABLED__PROJECTS__COLUMNS (default=["favored", "name", "project_status", "public", "created_at", "latest_activity_at", "required_disk_space"]) Columns in a projects list displayed by default
|
||||
OPENPROJECT_ENABLED__PROJECTS__COLUMNS (default=["favorited", "name", "project_status", "public", "created_at", "latest_activity_at", "required_disk_space"]) Columns in a projects list displayed by default
|
||||
OPENPROJECT_ENABLED__SCM (default=["subversion", "git"]) Enabled SCM
|
||||
OPENPROJECT_ENFORCE__TRACKING__START__AND__END__TIMES (default=false) Require start and finish times
|
||||
OPENPROJECT_ENTERPRISE__CHARGEBEE__SITE (default="openproject-enterprise") Site name for EE trial service
|
||||
|
||||
+1
-1
@@ -50,7 +50,7 @@ export class WidgetProjectFavoritesComponent extends AbstractWidgetComponent imp
|
||||
|
||||
ngOnInit() {
|
||||
const filters = new ApiV3FilterBuilder();
|
||||
filters.add('favored', '=', true);
|
||||
filters.add('favorited', '=', true);
|
||||
filters.add('active', '=', true);
|
||||
|
||||
this.projects$ = this
|
||||
|
||||
+6
-6
@@ -43,8 +43,8 @@
|
||||
*ngIf="projects$ | async as projects"
|
||||
>
|
||||
<spot-text-field
|
||||
*ngIf="displayMode !== 'favored' || (favorites$ | async)?.length > 0"
|
||||
[placeholder]="displayMode === 'favored' ? text.search_favorites_placeholder : text.search_placeholder"
|
||||
*ngIf="displayMode !== 'favorited' || (favorites$ | async)?.length > 0"
|
||||
[placeholder]="displayMode === 'favorited' ? text.search_favorites_placeholder : text.search_placeholder"
|
||||
[(ngModel)]="searchableProjectListService.searchText"
|
||||
[ngModelOptions]="{standalone: true}"
|
||||
data-test-selector="op-header-project-select--search"
|
||||
@@ -66,7 +66,7 @@
|
||||
class="spot-list_active"
|
||||
[projects]="projects"
|
||||
[displayMode]="displayMode"
|
||||
[favored]="favorites$ | async"
|
||||
[favorited]="favorites$ | async"
|
||||
[searchText]="searchableProjectListService.searchText"
|
||||
[root]="true"
|
||||
data-list-root="true"
|
||||
@@ -74,7 +74,7 @@
|
||||
></ul>
|
||||
|
||||
<ng-template #noResultsTemplate>
|
||||
<div *ngIf="displayMode === 'favored' && (favorites$ | async).length === 0"
|
||||
<div *ngIf="displayMode === 'favorited' && (favorites$ | async).length === 0"
|
||||
class="op-header-project-select--no-favorites"
|
||||
>
|
||||
<svg
|
||||
@@ -89,9 +89,9 @@
|
||||
[textContent]="text.no_favorites_subtext"></span>
|
||||
</p>
|
||||
</div>
|
||||
<span *ngIf="!(displayMode === 'favored' && (favorites$ | async).length === 0)"
|
||||
<span *ngIf="!(displayMode === 'favorited' && (favorites$ | async).length === 0)"
|
||||
class="op-project-list-modal--no-results">
|
||||
{{ displayMode === 'favored' ? text.no_favorite_results : text.no_results }}
|
||||
{{ displayMode === 'favorited' ? text.no_favorite_results : text.no_results }}
|
||||
</span>
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
|
||||
+6
-6
@@ -110,7 +110,7 @@ export class OpHeaderProjectSelectComponent extends UntilDestroyedMixin implemen
|
||||
.apiV3Service
|
||||
.projects
|
||||
.signalled(
|
||||
ApiV3Filter('favored', '=', true),
|
||||
ApiV3Filter('favorited', '=', true),
|
||||
[
|
||||
'elements/id',
|
||||
],
|
||||
@@ -125,7 +125,7 @@ export class OpHeaderProjectSelectComponent extends UntilDestroyedMixin implemen
|
||||
|
||||
public text = {
|
||||
all: this.I18n.t('js.label_all_uppercase'),
|
||||
favored: this.I18n.t('js.label_favorites'),
|
||||
favorited: this.I18n.t('js.label_favorites'),
|
||||
no_favorites: this.I18n.t('js.favorite_projects.no_results'),
|
||||
no_favorites_subtext: this.I18n.t('js.favorite_projects.no_results_subtext'),
|
||||
project: {
|
||||
@@ -140,11 +140,11 @@ export class OpHeaderProjectSelectComponent extends UntilDestroyedMixin implemen
|
||||
no_favorite_results: this.I18n.t('js.include_projects.no_favorite_results'),
|
||||
};
|
||||
|
||||
public displayMode:'all'|'favored';
|
||||
public displayMode:'all'|'favorited';
|
||||
|
||||
public displayModeOptions = [
|
||||
{ value: 'all', title: this.text.all },
|
||||
{ value: 'favored', title: this.text.favored },
|
||||
{ value: 'favorited', title: this.text.favorited },
|
||||
];
|
||||
|
||||
/* This seems like a way too convoluted loading check, but there's a good reason we need it.
|
||||
@@ -197,7 +197,7 @@ export class OpHeaderProjectSelectComponent extends UntilDestroyedMixin implemen
|
||||
}
|
||||
|
||||
ngOnInit():void {
|
||||
const stored = window.OpenProject.guardedLocalStorage(this.displayModeLocalStorageKey) as 'all'|'favored'|undefined;
|
||||
const stored = window.OpenProject.guardedLocalStorage(this.displayModeLocalStorageKey) as 'all'|'favorited'|undefined;
|
||||
this.displayMode = stored || 'all';
|
||||
}
|
||||
|
||||
@@ -211,7 +211,7 @@ export class OpHeaderProjectSelectComponent extends UntilDestroyedMixin implemen
|
||||
});
|
||||
}
|
||||
|
||||
displayModeChange(mode:'all'|'favored'):void {
|
||||
displayModeChange(mode:'all'|'favorited'):void {
|
||||
this.displayMode = mode;
|
||||
window.OpenProject.guardedLocalStorage(this.displayModeLocalStorageKey, mode);
|
||||
|
||||
|
||||
+2
-2
@@ -26,7 +26,7 @@
|
||||
[textContent]="project.name"></span>
|
||||
|
||||
<svg
|
||||
*ngIf="favored?.includes(project.id.toString())"
|
||||
*ngIf="favorited?.includes(project.id.toString())"
|
||||
star-fill-icon
|
||||
class="op-primer--star-icon"
|
||||
size="small"
|
||||
@@ -61,7 +61,7 @@
|
||||
op-header-project-select-list
|
||||
[projects]="project.children"
|
||||
[displayMode]="displayMode"
|
||||
[favored]="favored"
|
||||
[favorited]="favorited"
|
||||
[selected]="selected"
|
||||
[searchText]="searchText"
|
||||
></ul>
|
||||
|
||||
+8
-8
@@ -38,7 +38,7 @@ export class OpHeaderProjectSelectListComponent implements OnInit, OnChanges {
|
||||
|
||||
@Input() projects:IProjectData[] = [];
|
||||
|
||||
@Input() favored:string[] = [];
|
||||
@Input() favorited:string[] = [];
|
||||
|
||||
@Input() displayMode:string;
|
||||
|
||||
@@ -78,7 +78,7 @@ export class OpHeaderProjectSelectListComponent implements OnInit, OnChanges {
|
||||
}
|
||||
|
||||
ngOnChanges(changes:SimpleChanges) {
|
||||
if (changes.displayMode || changes.projects || changes.favored) {
|
||||
if (changes.displayMode || changes.projects || changes.favorited) {
|
||||
this.updateProjectFilter();
|
||||
}
|
||||
}
|
||||
@@ -89,20 +89,20 @@ export class OpHeaderProjectSelectListComponent implements OnInit, OnChanges {
|
||||
return true;
|
||||
}
|
||||
|
||||
return this.showWhenFavored(project);
|
||||
return this.showWhenFavorited(project);
|
||||
});
|
||||
}
|
||||
|
||||
showWhenFavored(project:IProjectData):boolean {
|
||||
if (this.isFavored(project)) {
|
||||
showWhenFavorited(project:IProjectData):boolean {
|
||||
if (this.isFavorited(project)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return project.children.length > 0 && project.children.some((child) => this.showWhenFavored(child));
|
||||
return project.children.length > 0 && project.children.some((child) => this.showWhenFavorited(child));
|
||||
}
|
||||
|
||||
isFavored(project:IProjectData):boolean {
|
||||
return this.favored.includes(project.id.toString());
|
||||
isFavorited(project:IProjectData):boolean {
|
||||
return this.favorited.includes(project.id.toString());
|
||||
}
|
||||
|
||||
extendedUrl(projectId:string|null):string {
|
||||
|
||||
@@ -31,8 +31,8 @@
|
||||
module OpenProject
|
||||
module Menu
|
||||
MenuGroup = Data.define(:header, :children)
|
||||
MenuItem = Data.define(:title, :href, :selected, :favored, :icon, :count, :show_enterprise_icon) do
|
||||
def initialize(title:, href:, selected:, favored: false, icon: nil, count: nil, show_enterprise_icon: false)
|
||||
MenuItem = Data.define(:title, :href, :selected, :favorited, :icon, :count, :show_enterprise_icon) do
|
||||
def initialize(title:, href:, selected:, favorited: false, icon: nil, count: nil, show_enterprise_icon: false)
|
||||
super
|
||||
end
|
||||
end
|
||||
|
||||
+19
-19
@@ -28,50 +28,50 @@
|
||||
|
||||
module OpenProject
|
||||
module Acts
|
||||
module Favorable
|
||||
module Favoritable
|
||||
def self.included(base)
|
||||
base.extend ClassMethods
|
||||
end
|
||||
|
||||
module ClassMethods
|
||||
# Marks an ActiveRecord::Model as favorable
|
||||
# A favorable model has association with users (watchers) that marked it as favorite.
|
||||
# Marks an ActiveRecord::Model as favoritable
|
||||
# A favoritable model has association with users (watchers) that marked it as favorite.
|
||||
#
|
||||
# This also creates the routes necessary for favoring/unfavoring by
|
||||
# This also creates the routes necessary for favoriting/unfavoriting by
|
||||
# adding the model's name to routes. This e.g leads to the following
|
||||
# routes when marking issues as watchable:
|
||||
# POST: projects/identifier/favorite
|
||||
# DELETE: projects/identifier/favorite
|
||||
#
|
||||
# acts_as_favorable expects that the including module defines a +visible?(user)+ method,
|
||||
# acts_as_favoritable 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 # rubocop:disable Metrics/AbcSize
|
||||
def acts_as_favoritable # rubocop:disable Metrics/AbcSize
|
||||
return if included_modules.include?(InstanceMethods)
|
||||
|
||||
class_eval do
|
||||
prepend InstanceMethods
|
||||
|
||||
has_many :favorites, as: :favored, dependent: :delete_all, validate: false
|
||||
has_many :favoring_users, through: :favorites, source: :user, validate: false
|
||||
has_many :favorites, as: :favorited, dependent: :delete_all, validate: false
|
||||
has_many :favoriting_users, through: :favorites, source: :user, validate: false
|
||||
|
||||
scope :favored_by, ->(user_id) {
|
||||
scope :favorited_by, ->(user_id) {
|
||||
includes(:favorites)
|
||||
.where(favorites: { user_id: })
|
||||
}
|
||||
|
||||
scope :with_favored_by_user, ->(user) {
|
||||
scope :with_favorited_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[:favorited_type].eq(base_class.name),
|
||||
favorite[:favorited_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)
|
||||
select(arel_table[Arel.star], "(favorites.id IS NOT NULL) AS favorited").joins(join)
|
||||
}
|
||||
end
|
||||
|
||||
@@ -80,21 +80,21 @@ module OpenProject
|
||||
end
|
||||
|
||||
module InstanceMethods
|
||||
def add_favoring_user(user)
|
||||
def add_favoriting_user(user)
|
||||
return if favorites.exists?(user_id: user.id)
|
||||
|
||||
favorites << Favorite.new(user:, favored: self)
|
||||
favorites << Favorite.new(user:, favorited: self)
|
||||
end
|
||||
|
||||
def remove_favoring_user(user)
|
||||
def remove_favoriting_user(user)
|
||||
favorites.where(user:).delete_all
|
||||
end
|
||||
|
||||
def set_favored(user, favored: true)
|
||||
favored ? add_favoring_user(user) : remove_favoring_user(user)
|
||||
def set_favorited(user, favorited: true)
|
||||
favorited ? add_favoriting_user(user) : remove_favoriting_user(user)
|
||||
end
|
||||
|
||||
def favored_by?(user)
|
||||
def favorited_by?(user)
|
||||
favorites.exists?(user:)
|
||||
end
|
||||
end
|
||||
+1
-1
@@ -28,7 +28,7 @@
|
||||
|
||||
module OpenProject
|
||||
module Acts
|
||||
module Favorable
|
||||
module Favoritable
|
||||
module Registry
|
||||
extend RegistryMethods
|
||||
end
|
||||
+1
-1
@@ -28,7 +28,7 @@
|
||||
|
||||
module OpenProject
|
||||
module Acts
|
||||
module Favorable
|
||||
module Favoritable
|
||||
module RouteConstraint
|
||||
def self.matches?(request)
|
||||
params = request.path_parameters
|
||||
@@ -27,8 +27,8 @@ MenuGroup = Data.define(:header, :children)
|
||||
|
||||
* `OpenProject::Menu::MenuItem`: A concrete Item with all options it supports
|
||||
```ruby
|
||||
MenuItem = Data.define(:title, :href, :selected, :favored, :icon, :count, :show_enterprise_icon) do
|
||||
def initialize(title:, href:, selected:, favored: false, icon: nil, count: nil, show_enterprise_icon: false)
|
||||
MenuItem = Data.define(:title, :href, :selected, :favorited, :icon, :count, :show_enterprise_icon) do
|
||||
def initialize(title:, href:, selected:, favorited: false, icon: nil, count: nil, show_enterprise_icon: false)
|
||||
super
|
||||
end
|
||||
end
|
||||
|
||||
@@ -42,13 +42,13 @@ module OpenProject
|
||||
# @display min_height 450px
|
||||
# @param searchable [Boolean]
|
||||
# @param with_create_button [Boolean]
|
||||
# @param favored [Boolean]
|
||||
# @param favorited [Boolean]
|
||||
# @param count [Integer]
|
||||
# @param show_enterprise_icon [Boolean]
|
||||
# @param icon [Symbol] octicon
|
||||
def playground(searchable: false,
|
||||
with_create_button: false,
|
||||
favored: false,
|
||||
favorited: false,
|
||||
count: nil,
|
||||
show_enterprise_icon: false,
|
||||
icon: nil)
|
||||
@@ -56,7 +56,7 @@ module OpenProject
|
||||
locals: {
|
||||
searchable:,
|
||||
create_btn_options: with_create_button ? { href: "/#", module_key: "user" } : nil,
|
||||
favored:,
|
||||
favorited:,
|
||||
count:,
|
||||
show_enterprise_icon:,
|
||||
icon:
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
OpenProject::Menu::MenuGroup.new(
|
||||
header: "Private",
|
||||
children: [
|
||||
OpenProject::Menu::MenuItem.new(title: "My query", href: "", selected: false, favored: favored),
|
||||
OpenProject::Menu::MenuItem.new(title: "My query", href: "", selected: false, favorited: favorited),
|
||||
OpenProject::Menu::MenuItem.new(title: "An awesome query", href: "", selected: false, count: count, icon: icon),
|
||||
OpenProject::Menu::MenuItem.new(title: "A third query", href: "", selected: false, show_enterprise_icon: show_enterprise_icon)
|
||||
]
|
||||
|
||||
@@ -7,17 +7,17 @@
|
||||
t("overviews.label")
|
||||
]
|
||||
)
|
||||
favored = @project.favored_by?(User.current)
|
||||
favorited = @project.favorited_by?(User.current)
|
||||
header.with_action_icon_button(
|
||||
icon: favored ? "star-fill" : "star",
|
||||
mobile_icon: favored ? "star-fill" : "star",
|
||||
icon: favorited ? "star-fill" : "star",
|
||||
mobile_icon: favorited ? "star-fill" : "star",
|
||||
size: :medium,
|
||||
tag: :a,
|
||||
href: build_favorite_path(@project, format: :html),
|
||||
data: { method: favored ? :delete : :post },
|
||||
classes: favored ? "op-primer--star-icon" : "",
|
||||
label: favored ? I18n.t(:button_unfavorite) : I18n.t(:button_favorite),
|
||||
aria: { label: favored ? I18n.t(:button_unfavorite) : I18n.t(:button_favorite) },
|
||||
data: { method: favorited ? :delete : :post },
|
||||
classes: favorited ? "op-primer--star-icon" : "",
|
||||
label: favorited ? I18n.t(:button_unfavorite) : I18n.t(:button_favorite),
|
||||
aria: { label: favorited ? I18n.t(:button_unfavorite) : I18n.t(:button_favorite) },
|
||||
test_selector: "project-favorite-button"
|
||||
)
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@ RSpec.describe Projects::RowComponent, type: :component do
|
||||
let(:project) { build_stubbed(:project, name: "My Project No. 1", identifier: "myproject_no_1") }
|
||||
let(:table) do
|
||||
instance_double(Projects::TableComponent, columns: [Queries::Projects::Selects::Default.new(:name)],
|
||||
favored_project_ids: [])
|
||||
favorited_project_ids: [])
|
||||
end
|
||||
|
||||
let(:user) { build_stubbed(:user) }
|
||||
|
||||
@@ -66,17 +66,17 @@ RSpec.describe "Favorite projects", :js, :selenium do
|
||||
expect(page).to have_css "a", accessible_name: "Remove from favorite"
|
||||
|
||||
project.reload
|
||||
expect(project).to be_favored_by(user)
|
||||
expect(project).to be_favorited_by(user)
|
||||
|
||||
projects_page.visit!
|
||||
projects_page.open_filters
|
||||
projects_page.filter_by_favored "yes"
|
||||
projects_page.filter_by_favorited "yes"
|
||||
|
||||
expect(page).to have_text "My favorite!"
|
||||
|
||||
projects_page.visit!
|
||||
projects_page.open_filters
|
||||
projects_page.filter_by_favored "no"
|
||||
projects_page.filter_by_favorited "no"
|
||||
|
||||
expect(page).to have_no_text "My favorite!"
|
||||
|
||||
@@ -114,10 +114,10 @@ RSpec.describe "Favorite projects", :js, :selenium do
|
||||
top_menu.expect_current_mode "All"
|
||||
end
|
||||
|
||||
context "when project is favored" do
|
||||
context "when project is favorited" do
|
||||
before do
|
||||
project.add_favoring_user(user)
|
||||
other_project.add_favoring_user(user)
|
||||
project.add_favoriting_user(user)
|
||||
other_project.add_favoriting_user(user)
|
||||
other_project.update! active: false
|
||||
end
|
||||
|
||||
@@ -137,7 +137,7 @@ RSpec.describe "Favorite projects", :js, :selenium do
|
||||
context "when favoriting only one subproject" do
|
||||
before do
|
||||
project.update! parent: other_project
|
||||
project.add_favoring_user(user)
|
||||
project.add_favoriting_user(user)
|
||||
end
|
||||
|
||||
it "still shows up in top menu (Regression #54729)" do
|
||||
@@ -171,7 +171,7 @@ RSpec.describe "Favorite projects", :js, :selenium do
|
||||
ProjectRole.anonymous.update permissions: [:view_project]
|
||||
end
|
||||
|
||||
it "does not shows favored projects" do
|
||||
it "does not shows favorited projects" do
|
||||
visit project_path(project)
|
||||
|
||||
retry_block do
|
||||
|
||||
@@ -239,7 +239,7 @@ RSpec.describe "Projects lists table display and actions", :js, with_settings: {
|
||||
end
|
||||
|
||||
visit project_path(project)
|
||||
expect(project).to be_favored_by(admin)
|
||||
expect(project).to be_favorited_by(admin)
|
||||
|
||||
visit projects_path
|
||||
projects_page.activate_menu_of(project) do |menu|
|
||||
@@ -248,7 +248,7 @@ RSpec.describe "Projects lists table display and actions", :js, with_settings: {
|
||||
end
|
||||
|
||||
visit project_path(project)
|
||||
expect(project).not_to be_favored_by(admin)
|
||||
expect(project).not_to be_favorited_by(admin)
|
||||
|
||||
visit projects_path
|
||||
projects_page.within_row(project) do
|
||||
@@ -260,7 +260,7 @@ RSpec.describe "Projects lists table display and actions", :js, with_settings: {
|
||||
projects_page.activate_menu_of(project) do |menu|
|
||||
expect(menu).to have_text("Remove from favorites")
|
||||
end
|
||||
expect(project).to be_favored_by(admin)
|
||||
expect(project).to be_favorited_by(admin)
|
||||
|
||||
projects_page.within_row(project) do
|
||||
page.find_test_selector("project-list-favorite-button").click
|
||||
@@ -269,7 +269,7 @@ RSpec.describe "Projects lists table display and actions", :js, with_settings: {
|
||||
projects_page.activate_menu_of(project) do |menu|
|
||||
expect(menu).to have_text("Add to favorites")
|
||||
end
|
||||
expect(project).not_to be_favored_by(admin)
|
||||
expect(project).not_to be_favorited_by(admin)
|
||||
end
|
||||
|
||||
specify "project can be deleted" do
|
||||
|
||||
@@ -411,17 +411,17 @@ RSpec.describe "Persisted lists on projects index page",
|
||||
projects_page.expect_no_columns("Public")
|
||||
end
|
||||
|
||||
it "allows favoring persisted query" do
|
||||
projects_page.expect_sidebar_filter("Persisted query", favored: false)
|
||||
it "allows favoriting persisted query" do
|
||||
projects_page.expect_sidebar_filter("Persisted query", favorited: false)
|
||||
|
||||
projects_page.set_sidebar_filter("Persisted query")
|
||||
projects_page.expect_sidebar_filter("Persisted query", selected: true, favored: false)
|
||||
projects_page.expect_sidebar_filter("Persisted query", selected: true, favorited: false)
|
||||
|
||||
projects_page.mark_query_favorite
|
||||
projects_page.expect_sidebar_filter("Persisted query", selected: true, favored: true)
|
||||
projects_page.expect_sidebar_filter("Persisted query", selected: true, favorited: true)
|
||||
|
||||
projects_page.unmark_query_favorite
|
||||
projects_page.expect_sidebar_filter("Persisted query", selected: true, favored: false)
|
||||
projects_page.expect_sidebar_filter("Persisted query", selected: true, favorited: false)
|
||||
end
|
||||
|
||||
it "loads the query with a custom field filter (Regression#57298)" do
|
||||
|
||||
+1
-1
@@ -30,7 +30,7 @@
|
||||
|
||||
require "spec_helper"
|
||||
|
||||
RSpec.describe OpenProject::Acts::Favorable::RouteConstraint do
|
||||
RSpec.describe OpenProject::Acts::Favoritable::RouteConstraint do
|
||||
let(:request) { instance_double(ActionDispatch::Request, path_parameters:) }
|
||||
let(:path_parameters) { { object_id: id, object_type: type } }
|
||||
|
||||
@@ -78,7 +78,7 @@ RSpec.describe Projects::Menu do
|
||||
end
|
||||
|
||||
it "has the favorite projects item" do
|
||||
expect(children_menu_items).to include(have_attributes(title: I18n.t("projects.lists.favored")))
|
||||
expect(children_menu_items).to include(have_attributes(title: I18n.t("projects.lists.favorited")))
|
||||
end
|
||||
|
||||
it "has an archived projects item" do
|
||||
@@ -96,7 +96,7 @@ RSpec.describe Projects::Menu do
|
||||
end
|
||||
|
||||
it "has the favorite projects item" do
|
||||
expect(children_menu_items).to include(have_attributes(title: I18n.t("projects.lists.favored")))
|
||||
expect(children_menu_items).to include(have_attributes(title: I18n.t("projects.lists.favorited")))
|
||||
end
|
||||
|
||||
it "has no archived projects item" do
|
||||
@@ -136,7 +136,7 @@ RSpec.describe Projects::Menu do
|
||||
end
|
||||
|
||||
it "has no favorite projects item" do
|
||||
expect(children_menu_items).not_to include(have_attributes(title: I18n.t("projects.lists.favored")))
|
||||
expect(children_menu_items).not_to include(have_attributes(title: I18n.t("projects.lists.favorited")))
|
||||
end
|
||||
|
||||
it "has no archived projects item" do
|
||||
@@ -163,13 +163,13 @@ RSpec.describe Projects::Menu do
|
||||
end
|
||||
|
||||
before do
|
||||
favored_queries.each do |query|
|
||||
query.add_favoring_user(current_user)
|
||||
favorited_queries.each do |query|
|
||||
query.add_favoriting_user(current_user)
|
||||
end
|
||||
end
|
||||
|
||||
context "when no queries are favored" do
|
||||
let(:favored_queries) { [] }
|
||||
context "when no queries are favorited" do
|
||||
let(:favorited_queries) { [] }
|
||||
|
||||
it "orders persisted titles alphabetically" do
|
||||
expect(titles).to eq(
|
||||
@@ -183,8 +183,8 @@ RSpec.describe Projects::Menu do
|
||||
end
|
||||
end
|
||||
|
||||
context "when some queries are favored" do
|
||||
let(:favored_queries) do
|
||||
context "when some queries are favorited" do
|
||||
let(:favorited_queries) do
|
||||
[
|
||||
current_user_query,
|
||||
public_query,
|
||||
@@ -192,7 +192,7 @@ RSpec.describe Projects::Menu do
|
||||
]
|
||||
end
|
||||
|
||||
it "orders persisted titles by favor then alphabetically" do
|
||||
it "orders persisted titles by favorite then alphabetically" do
|
||||
expect(titles).to eq(
|
||||
[
|
||||
["Active projects", "My projects", "Favorite projects"],
|
||||
@@ -204,8 +204,8 @@ RSpec.describe Projects::Menu do
|
||||
end
|
||||
end
|
||||
|
||||
context "when all queries are favored" do
|
||||
let(:favored_queries) do
|
||||
context "when all queries are favorited" do
|
||||
let(:favorited_queries) do
|
||||
[
|
||||
current_user_query,
|
||||
another_current_user_query,
|
||||
|
||||
@@ -472,7 +472,7 @@ RSpec.describe Project do
|
||||
end
|
||||
end
|
||||
|
||||
it_behaves_like "acts_as_favorable included" do
|
||||
it_behaves_like "acts_as_favoritable included" do
|
||||
let(:instance) { project }
|
||||
end
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ require "services/base_services/behaves_like_create_service"
|
||||
|
||||
RSpec.describe Queries::Factory,
|
||||
"ProjectQuery",
|
||||
with_settings: { enabled_projects_columns: %w[favored name project_status] } do
|
||||
with_settings: { enabled_projects_columns: %w[favorited name project_status] } do
|
||||
before do
|
||||
scope = instance_double(ActiveRecord::Relation)
|
||||
|
||||
@@ -52,7 +52,7 @@ RSpec.describe Queries::Factory,
|
||||
build_stubbed(:project_query, name: "My query") do |query|
|
||||
query.order(id: :asc)
|
||||
query.where(:project_status_code, "=", [Project.status_codes[:on_track].to_s])
|
||||
query.select(:project_status, :name, :favored)
|
||||
query.select(:project_status, :name, :favorited)
|
||||
end
|
||||
end
|
||||
let(:custom_field) do
|
||||
@@ -343,7 +343,7 @@ RSpec.describe Queries::Factory,
|
||||
|
||||
it "has the enabled_project_columns columns as selects" do
|
||||
expect(find.selects.map(&:attribute))
|
||||
.to eq(%i[project_status name favored])
|
||||
.to eq(%i[project_status name favorited])
|
||||
end
|
||||
|
||||
it { is_expected.not_to be_changed }
|
||||
@@ -654,7 +654,7 @@ RSpec.describe Queries::Factory,
|
||||
|
||||
it "has the selects of the persisted query" do
|
||||
expect(find.selects.map(&:attribute))
|
||||
.to eq(%i[project_status name favored])
|
||||
.to eq(%i[project_status name favorited])
|
||||
end
|
||||
|
||||
it { is_expected.to be_changed }
|
||||
@@ -699,7 +699,7 @@ RSpec.describe Queries::Factory,
|
||||
|
||||
it "has the selects of the persisted query" do
|
||||
expect(find.selects.map(&:attribute))
|
||||
.to eq(%i[project_status name favored])
|
||||
.to eq(%i[project_status name favorited])
|
||||
end
|
||||
|
||||
it { is_expected.to be_changed }
|
||||
@@ -805,7 +805,7 @@ RSpec.describe Queries::Factory,
|
||||
|
||||
it "keeps selects" do
|
||||
expect(find.selects.map(&:attribute))
|
||||
.to eq(%i[project_status name favored])
|
||||
.to eq(%i[project_status name favorited])
|
||||
end
|
||||
|
||||
it { is_expected.to be_changed }
|
||||
|
||||
@@ -36,7 +36,7 @@ RSpec.describe ProjectQuery do
|
||||
shared_let(:user) { create(:user) }
|
||||
shared_let(:admin) { create(:admin) }
|
||||
|
||||
it_behaves_like "acts_as_favorable included" do
|
||||
it_behaves_like "acts_as_favoritable included" do
|
||||
let(:instance) { create(:project_query) }
|
||||
end
|
||||
|
||||
@@ -137,7 +137,7 @@ RSpec.describe ProjectQuery do
|
||||
id
|
||||
identifier
|
||||
name
|
||||
favored
|
||||
favorited
|
||||
public
|
||||
description
|
||||
hierarchy
|
||||
@@ -162,7 +162,7 @@ RSpec.describe ProjectQuery do
|
||||
id
|
||||
identifier
|
||||
name
|
||||
favored
|
||||
favorited
|
||||
public
|
||||
description
|
||||
hierarchy
|
||||
|
||||
@@ -230,56 +230,56 @@ RSpec.describe "API v3 Project resource index", content_type: :json do
|
||||
end
|
||||
end
|
||||
|
||||
context "when filtering for favored" do
|
||||
let(:favored_project) { create(:project) }
|
||||
let(:unfavored_project) { create(:project) }
|
||||
context "when filtering for favorited" do
|
||||
let(:favorited_project) { create(:project) }
|
||||
let(:unfavorited_project) { create(:project) }
|
||||
|
||||
let(:projects) { [favored_project, unfavored_project] }
|
||||
let(:projects) { [favorited_project, unfavorited_project] }
|
||||
|
||||
current_user do
|
||||
create(:user, member_with_roles: { favored_project => role,
|
||||
unfavored_project => role }) do |user|
|
||||
favored_project.set_favored(user)
|
||||
create(:user, member_with_roles: { favorited_project => role,
|
||||
unfavorited_project => role }) do |user|
|
||||
favorited_project.set_favorited(user)
|
||||
end
|
||||
end
|
||||
|
||||
context "when filtering for favorite projects" do
|
||||
let(:filters) do
|
||||
[{ favored: { operator: "=", values: ["t"] } }]
|
||||
[{ favorited: { operator: "=", values: ["t"] } }]
|
||||
end
|
||||
|
||||
it_behaves_like "API V3 collection response", 1, 1, "Project" do
|
||||
let(:elements) { [favored_project] }
|
||||
let(:elements) { [favorited_project] }
|
||||
end
|
||||
end
|
||||
|
||||
context "when filtering for nonfavorite projects" do
|
||||
let(:filters) do
|
||||
[{ favored: { operator: "=", values: ["f"] } }]
|
||||
[{ favorited: { operator: "=", values: ["f"] } }]
|
||||
end
|
||||
|
||||
it_behaves_like "API V3 collection response", 1, 1, "Project" do
|
||||
let(:elements) { [unfavored_project] }
|
||||
let(:elements) { [unfavorited_project] }
|
||||
end
|
||||
end
|
||||
|
||||
context "when not filtering for favorite projects" do
|
||||
let(:filters) do
|
||||
[{ favored: { operator: "!", values: ["t"] } }]
|
||||
[{ favorited: { operator: "!", values: ["t"] } }]
|
||||
end
|
||||
|
||||
it_behaves_like "API V3 collection response", 1, 1, "Project" do
|
||||
let(:elements) { [unfavored_project] }
|
||||
let(:elements) { [unfavorited_project] }
|
||||
end
|
||||
end
|
||||
|
||||
context "when not filtering for nonfavorite projects" do
|
||||
let(:filters) do
|
||||
[{ favored: { operator: "!", values: ["f"] } }]
|
||||
[{ favorited: { operator: "!", values: ["f"] } }]
|
||||
end
|
||||
|
||||
it_behaves_like "API V3 collection response", 1, 1, "Project" do
|
||||
let(:elements) { [favored_project] }
|
||||
let(:elements) { [favorited_project] }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -179,7 +179,7 @@ RSpec.describe ProjectQueries::SetAttributesService, type: :model do
|
||||
subject
|
||||
|
||||
expect(model_instance.selects.map(&:attribute))
|
||||
.to eql %i[favored name cf_1]
|
||||
.to eql %i[favorited name cf_1]
|
||||
end
|
||||
|
||||
it "assigns default selects for admin",
|
||||
@@ -191,7 +191,7 @@ RSpec.describe ProjectQueries::SetAttributesService, type: :model do
|
||||
subject
|
||||
|
||||
expect(model_instance.selects.map(&:attribute))
|
||||
.to eql %i[favored name created_at cf_1]
|
||||
.to eql %i[favorited name created_at cf_1]
|
||||
end
|
||||
# rubocop:enable Naming/VariableNumber
|
||||
end
|
||||
|
||||
@@ -38,16 +38,16 @@ module Components
|
||||
expect(page).to have_css('[data-test-selector="op-submenu"]')
|
||||
end
|
||||
|
||||
def expect_item(name, selected: false, favored: nil, visible: true)
|
||||
def expect_item(name, selected: false, favorited: nil, visible: true)
|
||||
within "#main-menu" do
|
||||
selected_specifier = selected ? ".selected" : ":not(.selected)"
|
||||
|
||||
if favored.nil?
|
||||
if favorited.nil?
|
||||
expect(page).to have_css(".op-submenu--item-action#{selected_specifier}", text: name, visible:)
|
||||
else
|
||||
item = page.find(".op-submenu--item-action#{selected_specifier}", text: name, visible:)
|
||||
|
||||
if favored
|
||||
if favorited
|
||||
expect(item).to have_css(".op-primer--star-icon")
|
||||
else
|
||||
expect(item).to have_no_css(".op-primer--star-icon")
|
||||
|
||||
@@ -86,8 +86,8 @@ module Pages
|
||||
expect(page).to have_css('[data-test-selector="project-query-name"]', text: name)
|
||||
end
|
||||
|
||||
def expect_sidebar_filter(filter_name, selected: false, favored: false, visible: true)
|
||||
submenu.expect_item(filter_name, selected:, favored:, visible:)
|
||||
def expect_sidebar_filter(filter_name, selected: false, favorited: false, visible: true)
|
||||
submenu.expect_item(filter_name, selected:, favorited:, visible:)
|
||||
end
|
||||
|
||||
def expect_no_sidebar_filter(filter_name)
|
||||
@@ -221,8 +221,8 @@ module Pages
|
||||
wait_for_reload
|
||||
end
|
||||
|
||||
def filter_by_favored(value)
|
||||
set_filter("favored", "Favorite", "is", [value])
|
||||
def filter_by_favorited(value)
|
||||
set_filter("favorited", "Favorite", "is", [value])
|
||||
wait_for_reload
|
||||
end
|
||||
|
||||
@@ -520,7 +520,7 @@ module Pages
|
||||
private
|
||||
|
||||
def boolean_filter?(filter)
|
||||
%w[active member_of favored public templated].include?(filter.to_s)
|
||||
%w[active member_of favorited public templated].include?(filter.to_s)
|
||||
end
|
||||
|
||||
def submenu
|
||||
|
||||
@@ -1,161 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
#-- copyright
|
||||
# OpenProject is an open source project management software.
|
||||
# Copyright (C) the OpenProject GmbH
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License version 3.
|
||||
#
|
||||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
|
||||
# Copyright (C) 2006-2013 Jean-Philippe Lang
|
||||
# Copyright (C) 2010-2013 the ChiliProject Team
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
#
|
||||
# See COPYRIGHT and LICENSE files for more details.
|
||||
#++
|
||||
|
||||
RSpec.shared_examples_for "acts_as_favorable included" do
|
||||
shared_let(:favoring_user) { create(:user) }
|
||||
shared_let(:other_user) { create(:user) }
|
||||
|
||||
before do
|
||||
Favorite.create(user: favoring_user, favored: instance)
|
||||
end
|
||||
|
||||
it { is_expected.to have_many(:favorites).dependent(:delete_all) }
|
||||
it { is_expected.to have_many(:favoring_users).through(:favorites) }
|
||||
|
||||
describe ".favored_by" do
|
||||
it "returns instance for favoring user" do
|
||||
expect(described_class.favored_by(favoring_user).to_a).to eq [instance]
|
||||
end
|
||||
|
||||
it "returns no instance for non favoring user" do
|
||||
expect(described_class.favored_by(other_user).to_a).not_to eq [instance]
|
||||
end
|
||||
end
|
||||
|
||||
describe ".with_favored_by_user" do
|
||||
subject { described_class.with_favored_by_user(user).to_a }
|
||||
|
||||
context "for favoring user" do
|
||||
let(:user) { favoring_user }
|
||||
|
||||
it "returns instance for favoring user" do
|
||||
expect(subject).to eq [instance]
|
||||
end
|
||||
|
||||
it "marks instance as favored" do
|
||||
expect(subject).to all(have_attributes(favored: true))
|
||||
end
|
||||
end
|
||||
|
||||
context "for non favoring user" do
|
||||
let(:user) { other_user }
|
||||
|
||||
it "returns instance for favoring user" do
|
||||
expect(subject).to eq [instance]
|
||||
end
|
||||
|
||||
it "marks instance as not favored" do
|
||||
expect(subject).to all(have_attributes(favored: false))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "#add_favoring_user" do
|
||||
context "for favoring user" do
|
||||
let(:user) { favoring_user }
|
||||
|
||||
it "does nothing" do
|
||||
expect do
|
||||
instance.add_favoring_user(user)
|
||||
end.not_to change { described_class.favored_by(user).to_a }.from([instance])
|
||||
end
|
||||
end
|
||||
|
||||
context "for non favoring user" do
|
||||
let(:user) { other_user }
|
||||
|
||||
it "adds to favorites" do
|
||||
expect do
|
||||
instance.add_favoring_user(user)
|
||||
end.to change { described_class.favored_by(user).to_a }.from([]).to([instance])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "#remove_favoring_user" do
|
||||
context "for favoring user" do
|
||||
let(:user) { favoring_user }
|
||||
|
||||
it "removes from favorites" do
|
||||
expect do
|
||||
instance.remove_favoring_user(user)
|
||||
end.to change { described_class.favored_by(user).to_a }.from([instance]).to([])
|
||||
end
|
||||
end
|
||||
|
||||
context "for non favoring user" do
|
||||
let(:user) { other_user }
|
||||
|
||||
it "does nothing" do
|
||||
expect do
|
||||
instance.remove_favoring_user(user)
|
||||
end.not_to change { described_class.favored_by(user).to_a }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "#set_favored" do
|
||||
before do
|
||||
allow(instance).to receive(:add_favoring_user)
|
||||
allow(instance).to receive(:remove_favoring_user)
|
||||
end
|
||||
|
||||
it "calls add_favoring_user by default" do
|
||||
instance.set_favored(favoring_user)
|
||||
|
||||
expect(instance).to have_received(:add_favoring_user).with(favoring_user)
|
||||
expect(instance).not_to have_received(:remove_favoring_user)
|
||||
end
|
||||
|
||||
it "calls add_favoring_user when called with favored: true" do
|
||||
instance.set_favored(favoring_user, favored: true)
|
||||
|
||||
expect(instance).to have_received(:add_favoring_user).with(favoring_user)
|
||||
expect(instance).not_to have_received(:remove_favoring_user)
|
||||
end
|
||||
|
||||
it "calls remove_favoring_user when called with favored: false" do
|
||||
instance.set_favored(favoring_user, favored: false)
|
||||
|
||||
expect(instance).not_to have_received(:add_favoring_user)
|
||||
expect(instance).to have_received(:remove_favoring_user).with(favoring_user)
|
||||
end
|
||||
end
|
||||
|
||||
describe "#favored_by?" do
|
||||
it "returns true for favoring user" do
|
||||
expect(instance).to be_favored_by(favoring_user)
|
||||
end
|
||||
|
||||
it "returns false for non favoring user" do
|
||||
expect(instance).not_to be_favored_by(other_user)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,161 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
#-- copyright
|
||||
# OpenProject is an open source project management software.
|
||||
# Copyright (C) the OpenProject GmbH
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License version 3.
|
||||
#
|
||||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
|
||||
# Copyright (C) 2006-2013 Jean-Philippe Lang
|
||||
# Copyright (C) 2010-2013 the ChiliProject Team
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
#
|
||||
# See COPYRIGHT and LICENSE files for more details.
|
||||
#++
|
||||
|
||||
RSpec.shared_examples_for "acts_as_favoritable included" do
|
||||
shared_let(:favoriting_user) { create(:user) }
|
||||
shared_let(:other_user) { create(:user) }
|
||||
|
||||
before do
|
||||
Favorite.create(user: favoriting_user, favorited: instance)
|
||||
end
|
||||
|
||||
it { is_expected.to have_many(:favorites).dependent(:delete_all) }
|
||||
it { is_expected.to have_many(:favoriting_users).through(:favorites) }
|
||||
|
||||
describe ".favorited_by" do
|
||||
it "returns instance for favoriting user" do
|
||||
expect(described_class.favorited_by(favoriting_user).to_a).to eq [instance]
|
||||
end
|
||||
|
||||
it "returns no instance for non favoriting user" do
|
||||
expect(described_class.favorited_by(other_user).to_a).not_to eq [instance]
|
||||
end
|
||||
end
|
||||
|
||||
describe ".with_favorited_by_user" do
|
||||
subject { described_class.with_favorited_by_user(user).to_a }
|
||||
|
||||
context "for favoriting user" do
|
||||
let(:user) { favoriting_user }
|
||||
|
||||
it "returns instance for favoriting user" do
|
||||
expect(subject).to eq [instance]
|
||||
end
|
||||
|
||||
it "marks instance as favorited" do
|
||||
expect(subject).to all(have_attributes(favorited: true))
|
||||
end
|
||||
end
|
||||
|
||||
context "for non favoriting user" do
|
||||
let(:user) { other_user }
|
||||
|
||||
it "returns instance for favoriting user" do
|
||||
expect(subject).to eq [instance]
|
||||
end
|
||||
|
||||
it "marks instance as not favorited" do
|
||||
expect(subject).to all(have_attributes(favorited: false))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "#add_favoriting_user" do
|
||||
context "for favoriting user" do
|
||||
let(:user) { favoriting_user }
|
||||
|
||||
it "does nothing" do
|
||||
expect do
|
||||
instance.add_favoriting_user(user)
|
||||
end.not_to change { described_class.favorited_by(user).to_a }.from([instance])
|
||||
end
|
||||
end
|
||||
|
||||
context "for non favoriting user" do
|
||||
let(:user) { other_user }
|
||||
|
||||
it "adds to favorites" do
|
||||
expect do
|
||||
instance.add_favoriting_user(user)
|
||||
end.to change { described_class.favorited_by(user).to_a }.from([]).to([instance])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "#remove_favoriting_user" do
|
||||
context "for favoriting user" do
|
||||
let(:user) { favoriting_user }
|
||||
|
||||
it "removes from favorites" do
|
||||
expect do
|
||||
instance.remove_favoriting_user(user)
|
||||
end.to change { described_class.favorited_by(user).to_a }.from([instance]).to([])
|
||||
end
|
||||
end
|
||||
|
||||
context "for non favoriting user" do
|
||||
let(:user) { other_user }
|
||||
|
||||
it "does nothing" do
|
||||
expect do
|
||||
instance.remove_favoriting_user(user)
|
||||
end.not_to change { described_class.favorited_by(user).to_a }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "#set_favorited" do
|
||||
before do
|
||||
allow(instance).to receive(:add_favoriting_user)
|
||||
allow(instance).to receive(:remove_favoriting_user)
|
||||
end
|
||||
|
||||
it "calls add_favoriting_user by default" do
|
||||
instance.set_favorited(favoriting_user)
|
||||
|
||||
expect(instance).to have_received(:add_favoriting_user).with(favoriting_user)
|
||||
expect(instance).not_to have_received(:remove_favoriting_user)
|
||||
end
|
||||
|
||||
it "calls add_favoriting_user when called with favorited: true" do
|
||||
instance.set_favorited(favoriting_user, favorited: true)
|
||||
|
||||
expect(instance).to have_received(:add_favoriting_user).with(favoriting_user)
|
||||
expect(instance).not_to have_received(:remove_favoriting_user)
|
||||
end
|
||||
|
||||
it "calls remove_favoriting_user when called with favorited: false" do
|
||||
instance.set_favorited(favoriting_user, favorited: false)
|
||||
|
||||
expect(instance).not_to have_received(:add_favoriting_user)
|
||||
expect(instance).to have_received(:remove_favoriting_user).with(favoriting_user)
|
||||
end
|
||||
end
|
||||
|
||||
describe "#favorited_by?" do
|
||||
it "returns true for favoriting user" do
|
||||
expect(instance).to be_favorited_by(favoriting_user)
|
||||
end
|
||||
|
||||
it "returns false for non favoriting user" do
|
||||
expect(instance).not_to be_favorited_by(other_user)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -477,12 +477,12 @@ RSpec.describe Principals::DeleteJob, type: :model do
|
||||
|
||||
describe "favorites" do
|
||||
before do
|
||||
project.add_favoring_user(principal)
|
||||
project.add_favoriting_user(principal)
|
||||
job
|
||||
end
|
||||
|
||||
it "removes the assigned_to association to the principal" do
|
||||
expect(project.favoring_users.reload).to be_empty
|
||||
expect(project.favoriting_users.reload).to be_empty
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user