Add favorites

This commit is contained in:
Oliver Günther
2024-04-04 11:30:44 +02:00
parent dbc4fcc0b6
commit e96d2e6506
35 changed files with 524 additions and 162 deletions
@@ -47,7 +47,8 @@ class Projects::ProjectsFiltersComponent < FiltersComponent
Queries::Projects::Filters::CreatedAtFilter,
Queries::Projects::Filters::LatestActivityAtFilter,
Queries::Projects::Filters::NameAndIdentifierFilter,
Queries::Projects::Filters::TypeFilter
Queries::Projects::Filters::TypeFilter,
Queries::Projects::Filters::FavoredFilter
]
allowlist << Queries::Filters::Shared::CustomFields::Base if EnterpriseToken.allows_to?(:custom_fields_in_projects_list)
+8
View File
@@ -29,6 +29,8 @@
#++
module Projects
class RowComponent < ::RowComponent
delegate :favored_projects, to: :table
def project
model.first
end
@@ -42,6 +44,12 @@ module Projects
""
end
def favored
if favored_projects.include?(project.id)
render(Primer::Beta::Octicon.new(icon: "star-fill", color: :attention, "aria-label": I18n.t(:label_favoured)))
end
end
def column_value(column)
if custom_field_column?(column)
custom_field_column(column)
@@ -64,7 +64,11 @@ See COPYRIGHT and LICENSE files for more details.
<div class="generic-table--sort-header-outer">
<div class="generic-table--sort-header">
<span>
<% if column.attribute == :favored %>
<%= render(Primer::Beta::Octicon.new(icon: "star-fill", color: :subtle, "aria-label": I18n.t(:label_favoured))) %>
<% else %>
<%= column.caption %>
<% end %>
</span>
</div>
</div>
@@ -153,6 +153,10 @@ module Projects
end
end
def favored_projects
@favored_projects ||= Favorite.where(user: current_user, favored_type: 'Project').pluck(:favored_id)
end
def sorted_by_lft?
query.orders.first&.attribute == :lft
end
+49 -21
View File
@@ -1,34 +1,62 @@
class AnnouncementsController < ApplicationController
layout "admin"
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2024 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.
#++
before_action :require_admin
class FavoritesController < ApplicationController
before_action :find_favored_by_object
before_action :require_login
def edit
@announcement = Announcement.only_one
def favorite
if @favored.visible?(User.current)
set_favored(User.current, true)
else
render_403
end
end
def update
@announcement = Announcement.only_one
@announcement.attributes = announcement_params
if @announcement.save
flash[:notice] = t(:notice_successful_update)
end
redirect_to action: "edit"
def unfavorite
set_favored(User.current, false)
end
private
def default_breadcrumb
t(:label_announcement)
def find_favored_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
end
def show_local_breadcrumb
true
end
def set_favored(user, favored)
@favored.set_favored(user, favored:)
def announcement_params
params.require(:announcement).permit("text", "show_until", "active")
respond_to do |format|
format.html { redirect_back(fallback_location: home_url) }
format.json { head :no_content }
end
end
end
+2
View File
@@ -1,5 +1,7 @@
class ApplicationRecord < ActiveRecord::Base
include ::OpenProject::Acts::Watchable
include ::OpenProject::Acts::Favorable
self.abstract_class = true
+35
View File
@@ -0,0 +1,35 @@
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2024 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.
#++
class Favorite < ApplicationRecord
belongs_to :user
belongs_to :favored, polymorphic: true
validates :user, presence: true
validates :favored, presence: true
end
+2 -1
View File
@@ -33,7 +33,6 @@ class Project < ApplicationRecord
include Projects::Activity
include Projects::Hierarchy
include Projects::AncestorsFromRoot
include ::Scopes::Scoped
include Projects::ActsAsCustomizablePatches
@@ -90,6 +89,8 @@ class Project < ApplicationRecord
has_many :project_storages, dependent: :destroy, class_name: 'Storages::ProjectStorage'
has_many :storages, through: :project_storages
acts_as_favorable
acts_as_customizable # partially overridden via Projects::ActsAsCustomizablePatches in order to support sections and
# project-leval activation of custom fields
+2
View File
@@ -46,6 +46,7 @@ module Queries::Projects
filter Filters::ProjectStatusFilter
filter Filters::UserActionFilter
filter Filters::VisibleFilter
filter Filters::FavoredFilter
order Orders::DefaultOrder
order Orders::LatestActivityAtOrder
@@ -61,5 +62,6 @@ module Queries::Projects
select Selects::LatestActivityAt
select Selects::RequiredDiskSpace
select Selects::Status
select Selects::Favored
end
end
+1 -1
View File
@@ -100,7 +100,7 @@ class Queries::Projects::Factory
def list_with(name)
Queries::Projects::ProjectQuery.new(name: I18n.t(name)) do |query|
query.order("lft" => "asc")
query.select(*(["name"] + Setting.enabled_projects_columns).uniq, add_not_existing: false)
query.select(*(["favored", "name"] + Setting.enabled_projects_columns).uniq, add_not_existing: false)
yield query
end
@@ -0,0 +1,58 @@
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2024 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.
#++
class Queries::Projects::Filters::FavoredFilter < Queries::Projects::Filters::ProjectFilter
include Queries::Filters::Shared::BooleanFilter
def self.key
:favored
end
def available?
User.current.logged?
end
def scope
if values.first == OpenProject::Database::DB_VALUE_TRUE
super.where(id: favored_project_ids)
else
super.where.not(id: favored_project_ids)
end
end
# Handled by scope
def where
"1=1"
end
def favored_project_ids
Favorite
.where(favored_type: "Project", user_id: User.current.id)
.select(:favored_id)
end
end
@@ -0,0 +1,41 @@
# -- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2010-2024 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.
# ++
class Queries::Projects::Selects::Favored < Queries::Selects::Base
def self.key
:favored
end
def self.available?
true
end
def caption
I18n.t(:label_favoured)
end
end
@@ -92,6 +92,6 @@ class Queries::Projects::ProjectQueries::SetAttributesService < BaseServices::Se
end
def default_columns
(["name"] + Setting.enabled_projects_columns).uniq
(["favored", "name"] + Setting.enabled_projects_columns).uniq
end
end
+8
View File
@@ -0,0 +1,8 @@
# Be sure to restart your server when you modify this file.
# For development and non-eager load mode, we need to register the acts_as_favorable models manually
# as no eager loading takes place
Rails.application.config.after_initialize do
OpenProject::Acts::Favorable::Registry
.add(Project)
end
+1 -3
View File
@@ -1,10 +1,8 @@
# Be sure to restart your server when you modify this file.
# In development mode, we need to register the acts_as_watchable models manually
# In development and non-eager loaded mode, we need to register the acts_as_watchable models manually
# as no eager loading takes place
Rails.application.config.after_initialize do
if Rails.env.development?
OpenProject::Acts::Watchable::Registry
.add(WorkPackage, Message, Forum, News, Meeting, Wiki, WikiPage)
end
end
+1
View File
@@ -38,3 +38,4 @@ require_relative "../../lib_static/open_project/feature_decisions"
# initializer 'the_engine.feature_decisions' do
# OpenProject::FeatureDecisions.add :some_flag
# end
OpenProject::FeatureDecisions.add :favorite_projects
+2
View File
@@ -1346,6 +1346,7 @@ Project attributes and sections are defined in the <a href=%{admin_settings_url}
button_edit: "Edit"
button_edit_associated_wikipage: "Edit associated Wiki page: %{page_title}"
button_expand_all: "Expand all"
button_favorite: "Mark as favorite"
button_filter: "Filter"
button_generate: "Generate"
button_list: "List"
@@ -1373,6 +1374,7 @@ Project attributes and sections are defined in the <a href=%{admin_settings_url}
button_unarchive: "Unarchive"
button_uncheck_all: "Uncheck all"
button_unlock: "Unlock"
button_unfavorite: "Remove from favorites"
button_unwatch: "Unwatch"
button_update: "Update"
button_upgrade: "Upgrade"
+6 -1
View File
@@ -167,12 +167,17 @@ Rails.application.routes.draw do
resource :wiki_menu_item, only: %i[edit update]
end
# generic route for adding/removing watchers and favorites.
scope ":object_type/:object_id", constraints: OpenProject::Acts::Watchable::Routes do
post "/watch" => "watchers#watch"
delete "/unwatch" => "watchers#unwatch"
end
# generic route for adding/removing watchers and favorites.
scope ":object_type/:object_id", constraints: OpenProject::Acts::Favorable::Routes do
post "/favorite" => "favorites#favorite"
delete "/favorite" => "favorites#unfavorite"
end
namespace :projects do
resource :menu, only: %i[show]
resources :queries, only: %i[new create destroy]
@@ -0,0 +1,13 @@
class CreateFavorite < ActiveRecord::Migration[7.1]
def change
create_table :favorites do |t|
t.references :user, null: false, foreign_key: true, index: true
t.references :favored, null: false, polymorphic: true
t.timestamps
end
add_index :favorites, %i[favored_type favored_id]
add_index :favorites, %i[user_id favored_type favored_id], unique: true
end
end
@@ -10,6 +10,8 @@ import { GRID_PROVIDERS } from 'core-app/shared/components/grids/grid/grid.compo
providers: GRID_PROVIDERS,
})
export class OverviewComponent extends GridPageComponent {
showToolbar = false;
protected i18nNamespace():string {
return 'overviews';
}
@@ -1,5 +1,6 @@
<div class="op-grid-page">
<div class="toolbar op-grid-page--toolbar-container">
<div *ngIf="showToolbar"
class="toolbar op-grid-page--toolbar-container">
<div class="op-grid-page--title-container title-container">
<h2 [textContent]="text.title"></h2>
</div>
@@ -1,6 +1,4 @@
import {
ChangeDetectorRef, Directive, OnDestroy, OnInit, Renderer2,
} from '@angular/core';
import { ChangeDetectorRef, Directive, OnDestroy, OnInit, Renderer2 } from '@angular/core';
import { I18nService } from 'core-app/core/i18n/i18n.service';
import { Title } from '@angular/platform-browser';
import { GridInitializationService } from 'core-app/shared/components/grids/grid/initialization.service';
@@ -18,6 +16,8 @@ export abstract class GridPageComponent implements OnInit, OnDestroy {
html_title: this.i18n.t(`js.${this.i18nNamespace()}.label`),
};
public showToolbar = true;
constructor(
readonly gridInitialization:GridInitializationService,
// not used in the base class but will be used throughout the subclasses
+25 -106
View File
@@ -34,59 +34,31 @@ module OpenProject
end
module ClassMethods
# Marks an ActiveRecord::Model as watchable
# A watchable model has association with users (watchers) who wish to
# be informed of changes on it.
# Marks an ActiveRecord::Model as favorable
# A favorable model has association with users (watchers) that marked it as favorite.
#
# This also creates the routes necessary for watching/unwatching by
# This also creates the routes necessary for favoring/unfavoring by
# adding the model's name to routes. This e.g leads to the following
# routes when marking issues as watchable:
# POST: issues/1/watch
# DELETE: issues/1/unwatch
# GET/POST: issues/1/watchers/new
# DELETE: issues/1/watchers/1
# POST: projects/identifier/favorite
# DELETE: projects/identifier/favorite
#
# params:
# options:
# permission: overrides the permission used to determine whether a user
# is allowed to watch
def acts_as_watchable(options = {})
return if included_modules.include?(::OpenProject::Acts::Watchable::InstanceMethods)
acts_as_watchable_enforce_project_association
# 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
return if included_modules.include?(::OpenProject::Acts::Favorable::InstanceMethods)
class_eval do
has_many :watchers, as: :watchable, dependent: :delete_all, validate: false
has_many :watcher_users, through: :watchers, source: :user, validate: false
has_many :favorites, as: :favored, dependent: :delete_all, validate: false
has_many :favoring_users, through: :favorites, source: :user, validate: false
scope :watched_by, ->(user_id) {
includes(:watchers)
.where(watchers: { user_id: })
scope :favored_by, ->(user_id) {
includes(:favorites)
.where(favorites: { user_id: })
}
class_attribute :acts_as_watchable_options
self.acts_as_watchable_options = options
end
send :prepend, OpenProject::OpenProject::Acts::Watchable::InstanceMethods
end
def acts_as_watchable_enforce_project_association
unless reflect_on_association(:project)
message = <<-MESSAGE
The #{self} model does not have an association to the Project model.
acts_as_watchable requires the including model to have such an association.
If no direct association exists, consider adding a
has_one :project, through: ...
association.
MESSAGE
raise message
end
send :prepend, ::OpenProject::Acts::Favorable::InstanceMethods
end
end
@@ -95,75 +67,22 @@ module OpenProject
base.extend ClassMethods
end
def possible_watcher?(user)
user.allowed_based_on_permission_context?(self.class.acts_as_watchable_permission,
project:,
entity: self)
def add_favoring_user(user)
return if favorites.exists?(user_id: user.id)
favorites << Favorite.new(user:, favored: self)
end
# Returns all users that could potentially be watchers.
# This includes those already added as watchers.
#
# Admins are excluded at least for non public projects
# because while they have the right to be added as watchers having
# them pop up in every project would be weird.
def possible_watcher_users
active_scope = Principal.not_locked.user
allowed_scope = if project.public?
User.allowed(self.class.acts_as_watchable_permission, project)
else
User.allowed_members_on_work_package(self.class.acts_as_watchable_permission, self)
end
active_scope.where(id: allowed_scope)
def remove_favoring_user(user)
favorites.where(user:).delete_all
end
# Returns an array of users that are proposed as watchers
def addable_watcher_users
possible_watcher_users.where.not(id: watcher_users.pluck(:id))
def set_favored(user, favored: true)
favored ? add_favoring_user(user) : remove_favoring_user(user)
end
# Adds user as a watcher
def add_watcher(user)
watchers << Watcher.new(user:, watchable: self) unless watchers.map(&:user_id).include?(user.id)
end
# Removes user from the watchers list
def remove_watcher(user)
return nil unless user&.is_a?(User)
watchers_to_delete = watchers.find_all { |watcher| watcher.user == user }
watchers_to_delete.each(&:delete)
watchers.reload
watchers_to_delete.count
end
# Adds/removes watcher
def set_watcher(user, watching = true)
watching ? add_watcher(user) : remove_watcher(user)
end
# Overrides watcher_user_ids= to make user_ids uniq
def watcher_user_ids=(user_ids)
if user_ids.is_a?(Array)
user_ids = user_ids.uniq
end
super
end
# Returns true if object is watched by +user+
def watched_by?(user)
user.present? &&
((watchers.loaded? && watchers.map(&:user_id).any? { |uid| uid == user.id }) ||
watcher_user_ids.any? { |uid| uid == user.id })
end
module ClassMethods
def acts_as_watchable_permission
acts_as_watchable_options[:permission] || :"view_#{name.underscore.pluralize}"
end
def favored_by?(user)
favorites.exists?(user:)
end
end
end
@@ -0,0 +1,57 @@
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2024 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.
#++
module OpenProject
module Acts
module Favorable
module Registry
def self.models
@models ||= Set.new
end
def self.exists?(model)
models.include?(model)
end
def self.instance(model_name)
models.detect { |cls| cls.name == model_name.singularize.camelize }
end
def self.add(*models)
models.each do |model|
unless model.ancestors.include?(::OpenProject::Acts::Watchable)
raise ArgumentError.new("Model #{model} does not include acts_as_watchable")
end
self.models << model
end
end
end
end
end
end
@@ -0,0 +1,42 @@
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2024 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.
#++
module OpenProject
module Acts
module Favorable
module Routes
def self.matches?(request)
params = request.path_parameters
Registry.instance(params[:object_type]) &&
/\d+/.match(params[:object_id])
end
end
end
end
end
@@ -39,7 +39,7 @@ module OpenProject
end
def self.instance(model_name)
models.detect { |cls| cls.name == model_name }
models.detect { |cls| cls.name == model_name.singularize.camelize }
end
def self.add(*models)
@@ -34,7 +34,7 @@ module OpenProject
def self.matches?(request)
params = request.path_parameters
Registry.exists?(params[:object_type]) &&
Registry.instance(params[:object_type]) &&
/\d+/.match(params[:object_id])
end
end
@@ -8,6 +8,10 @@ module ::Overviews
menu_item :overview
def show
render
end
def project_custom_fields_sidebar
render :project_custom_fields_sidebar, layout: false
end
@@ -1,3 +1,41 @@
<% content_for :header_tags do %>
<meta name="sidebar_enabled" data-enabled="<%= @sidebar_enabled %>">
<% end -%>
<%=
render(Primer::OpenProject::PageHeader.new) do |header|
header.with_title(variant: :medium) { t("overviews.label") }
header.with_breadcrumbs(
[
{ href: project_path(@project), text: @project.name },
t("overviews.label")
]
)
header.with_actions do
if OpenProject::FeatureDecisions.favorite_projects_active?
favored = @project.favored_by?(User.current)
primer_form_with(
url: favorite_path(object_id: @project.id, object_type: @project.model_name.route_key),
method: favored ? :delete : :post
) do |_form|
render(
Primer::Beta::IconButton.new(
icon: favored ? "star-fill" : "star",
size: :medium,
color: favored ? :attention : :default,
type: :submit,
aria: { label: favored ? I18n.t(:button_unfavorite) : I18n.t(:button_favorite) },
test_selector: 'project-favorite-button'
)
) do |button|
button.with_leading_visual_icon(icon: :trash)
I18n.t('button_delete')
end
end
end
end
end
%>
<openproject-base></openproject-base>
+67
View File
@@ -0,0 +1,67 @@
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2024 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.
#++
require "spec_helper"
require_relative "../../support/pages/my/page"
RSpec.describe "Project favorites",
:js,
with_flag: :favorite_projects do
shared_let(:project) { create(:project, name: 'My favorite!', enabled_module_names: []) }
let(:permissions) { %i(edit_project select_project_modules view_work_packages) }
let(:projects_page) { Pages::Projects::Index.new }
current_user do
create(:user, member_with_permissions: { project => permissions })
end
it "allows favoriting and unfavoriting projects" do
visit project_path(project)
expect(page).to have_selector :button, accessible_name: "Mark as favorite"
click_link_or_button(accessible_name: "Mark as favorite")
expect(page).to have_selector :button, accessible_name: "Remove from favorite"
project.reload
expect(project).to be_favored_by(current_user)
projects_page.visit!
projects_page.open_filters
projects_page.filter_by_favored "yes"
expect(page).to have_text 'My favorite!'
projects_page.visit!
projects_page.open_filters
projects_page.filter_by_favored "no"
expect(page).to have_no_text 'My favorite!'
end
end
+20 -17
View File
@@ -71,6 +71,9 @@ RSpec.describe Queries::Projects::Factory, with_settings: { enabled_projects_col
let(:id) { nil }
let(:params) { {} }
let(:default_selects) do
%i[favored] + Setting.enabled_projects_columns.map(&:to_sym)
end
current_user { build_stubbed(:user) }
@@ -100,7 +103,7 @@ RSpec.describe Queries::Projects::Factory, with_settings: { enabled_projects_col
it "has the enabled_project_columns columns as selects" do
expect(find.selects.map(&:attribute))
.to eq(Setting.enabled_projects_columns.map(&:to_sym))
.to eq(default_selects)
end
end
@@ -115,7 +118,7 @@ RSpec.describe Queries::Projects::Factory, with_settings: { enabled_projects_col
it "has the enabled_project_columns columns as selects" do
expect(find.selects.map(&:attribute))
.to eq(Setting.enabled_projects_columns.map(&:to_sym))
.to eq(default_selects)
end
end
@@ -144,7 +147,7 @@ RSpec.describe Queries::Projects::Factory, with_settings: { enabled_projects_col
it "has the enabled_project_columns columns as selects" do
expect(find.selects.map(&:attribute))
.to eq(Setting.enabled_projects_columns.map(&:to_sym))
.to eq(default_selects)
end
end
@@ -173,7 +176,7 @@ RSpec.describe Queries::Projects::Factory, with_settings: { enabled_projects_col
it "has the enabled_project_columns columns as selects" do
expect(find.selects.map(&:attribute))
.to eq(Setting.enabled_projects_columns.map(&:to_sym))
.to eq(default_selects)
end
end
@@ -202,7 +205,7 @@ RSpec.describe Queries::Projects::Factory, with_settings: { enabled_projects_col
it "has the enabled_project_columns columns as selects" do
expect(find.selects.map(&:attribute))
.to eq(Setting.enabled_projects_columns.map(&:to_sym))
.to eq(default_selects)
end
end
@@ -231,7 +234,7 @@ RSpec.describe Queries::Projects::Factory, with_settings: { enabled_projects_col
it "has the enabled_project_columns columns as selects" do
expect(find.selects.map(&:attribute))
.to eq(Setting.enabled_projects_columns.map(&:to_sym))
.to eq(default_selects)
end
end
@@ -260,7 +263,7 @@ RSpec.describe Queries::Projects::Factory, with_settings: { enabled_projects_col
it "has the enabled_project_columns columns as selects" do
expect(find.selects.map(&:attribute))
.to eq(Setting.enabled_projects_columns.map(&:to_sym))
.to eq(default_selects)
end
end
@@ -289,7 +292,7 @@ RSpec.describe Queries::Projects::Factory, with_settings: { enabled_projects_col
it "has the enabled_project_columns columns as selects" do
expect(find.selects.map(&:attribute))
.to eq(Setting.enabled_projects_columns.map(&:to_sym))
.to eq(default_selects)
end
end
@@ -407,7 +410,7 @@ RSpec.describe Queries::Projects::Factory, with_settings: { enabled_projects_col
it "has the enabled_project_columns columns as selects" do
expect(find.selects.map(&:attribute))
.to eq(Setting.enabled_projects_columns.map(&:to_sym))
.to eq(default_selects)
end
end
@@ -452,7 +455,7 @@ RSpec.describe Queries::Projects::Factory, with_settings: { enabled_projects_col
it "has the enabled_project_columns columns as selects" do
expect(find.selects.map(&:attribute))
.to eq(Setting.enabled_projects_columns.map(&:to_sym))
.to eq(default_selects)
end
end
@@ -657,7 +660,7 @@ RSpec.describe Queries::Projects::Factory, with_settings: { enabled_projects_col
it "has only the available fields (non admin only and only existing cf)" do
expect(find.selects.map(&:attribute))
.to eq(%i[name cf_1]) # rubocop:disable Naming/VariableNumber
.to eq(%i[favored name cf_1]) # rubocop:disable Naming/VariableNumber
end
end
@@ -725,7 +728,7 @@ RSpec.describe Queries::Projects::Factory, with_settings: { enabled_projects_col
it "has the enabled_project_columns columns as selects" do
expect(find.selects.map(&:attribute))
.to eq(Setting.enabled_projects_columns.map(&:to_sym))
.to eq(default_selects)
end
end
@@ -754,7 +757,7 @@ RSpec.describe Queries::Projects::Factory, with_settings: { enabled_projects_col
it "has the enabled_project_columns columns as selects" do
expect(find.selects.map(&:attribute))
.to eq(Setting.enabled_projects_columns.map(&:to_sym))
.to eq(default_selects)
end
end
@@ -783,7 +786,7 @@ RSpec.describe Queries::Projects::Factory, with_settings: { enabled_projects_col
it "has the enabled_project_columns columns as selects" do
expect(find.selects.map(&:attribute))
.to eq(Setting.enabled_projects_columns.map(&:to_sym))
.to eq(default_selects)
end
end
@@ -812,7 +815,7 @@ RSpec.describe Queries::Projects::Factory, with_settings: { enabled_projects_col
it "has the enabled_project_columns columns as selects" do
expect(find.selects.map(&:attribute))
.to eq(Setting.enabled_projects_columns.map(&:to_sym))
.to eq(default_selects)
end
end
@@ -841,7 +844,7 @@ RSpec.describe Queries::Projects::Factory, with_settings: { enabled_projects_col
it "has the enabled_project_columns columns as selects" do
expect(find.selects.map(&:attribute))
.to eq(Setting.enabled_projects_columns.map(&:to_sym))
.to eq(default_selects)
end
end
@@ -870,7 +873,7 @@ RSpec.describe Queries::Projects::Factory, with_settings: { enabled_projects_col
it "has the enabled_project_columns columns as selects" do
expect(find.selects.map(&:attribute))
.to eq(Setting.enabled_projects_columns.map(&:to_sym))
.to eq(default_selects)
end
end
end
@@ -108,6 +108,7 @@ RSpec.describe Queries::Projects::ProjectQuery do
it "lists registered selects" do
expect(instance.available_selects.map(&:attribute))
.to contain_exactly(:name,
:favored,
:public,
:description,
:hierarchy,
@@ -122,6 +123,7 @@ RSpec.describe Queries::Projects::ProjectQuery do
expect(instance.available_selects.map(&:attribute))
.to contain_exactly(:name,
:public,
:favored,
:description,
:hierarchy,
:project_status,
@@ -139,6 +141,7 @@ RSpec.describe Queries::Projects::ProjectQuery do
expect(instance.available_selects.map(&:attribute))
.to contain_exactly(:name,
:public,
:favored,
:description,
:hierarchy,
:project_status,
@@ -182,7 +182,7 @@ RSpec.describe Queries::Projects::ProjectQueries::SetAttributesService, type: :m
subject
expect(model_instance.selects.map(&:attribute))
.to eql Setting.enabled_projects_columns.map(&:to_sym)
.to eql %i[favored] + Setting.enabled_projects_columns.map(&:to_sym)
end
it "assigns default selects excluding those for admin and ee if not allowed",
@@ -190,7 +190,7 @@ RSpec.describe Queries::Projects::ProjectQueries::SetAttributesService, type: :m
subject
expect(model_instance.selects.map(&:attribute))
.to eql [:name]
.to eql [:favored, :name]
end
end
+10 -1
View File
@@ -177,6 +177,15 @@ module Pages
click_button "Apply"
end
def filter_by_favored(value)
set_filter("favored",
"Favored",
"is",
[value])
click_button "Apply"
end
def filter_by_membership(value)
set_filter("member_of",
"I am member",
@@ -376,7 +385,7 @@ module Pages
private
def boolean_filter?(filter)
%w[active member_of public templated].include?(filter.to_s)
%w[active member_of favored public templated].include?(filter.to_s)
end
end
end
+7 -1
View File
@@ -42,6 +42,12 @@ RSpec.configure do |config|
config.include WithFlagMixin
config.before :example, :with_flag do |example|
with_flags(example.metadata[:with_flag])
value = example.metadata[:with_flag]
case value
when Symbol
with_flags(value => true)
else
with_flags(value)
end
end
end