Skip to content

Commit

Permalink
Merge pull request #12185 from danidoni/new-watchlist-implementation
Browse files Browse the repository at this point in the history
Implement the new watchlist feature
  • Loading branch information
hennevogel committed Feb 21, 2022
2 parents bf78a42 + 529c8c3 commit 6b32cc4
Show file tree
Hide file tree
Showing 21 changed files with 509 additions and 3 deletions.
1 change: 1 addition & 0 deletions src/api/app/assets/stylesheets/webui/application.scss
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@
@import 'user_profile';
@import 'long-text';
@import 'workflow_runs';
@import 'new_watchlist/watchlist';

html {
overflow-y: scroll !important;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.color-inverted {
-webkit-filter: invert(100%); /* safari 6.0 - 9.0 */
filter: invert(100%);
}
18 changes: 18 additions & 0 deletions src/api/app/components/watched_items_list_component.html.haml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
%h5.mt-2.text-light= list_title
- if @items.any?
- @items.each do |item|
- case @class_name
- when 'Package'
= link_to(package_show_path(item.project, item), class: 'text-word-break-all') do
%i.fas.fa-archive.mr-1
#{item.project}/#{item}
- when 'Project'
= link_to(project_show_path(item), class: 'text-word-break-all') do
%i.fas.fa-cubes.mr-1
#{item}
- when 'BsRequest'
= link_to(request_show_path(number: item.number), class: 'text-word-break-all') do
= image_tag('icons/request-icon.svg', height: 18, class: 'mr-1 color-inverted')
Request ##{item.number}
- else
%p.text-muted= empty_list_text
30 changes: 30 additions & 0 deletions src/api/app/components/watched_items_list_component.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
class WatchedItemsListComponent < ApplicationComponent
LIST_TITLE = {
'Package' => 'Packages you are watching',
'Project' => 'Projects you are watching',
'BsRequest' => 'Requests you are watching'
}.freeze

EMPTY_LIST_TEXTS = {
'Package' => 'There are no packages in the watchlist yet.',
'Project' => 'There are no projects in the watchlist yet.',
'BsRequest' => 'There are no requests in the watchlist yet.'
}.freeze

def initialize(items:, class_name:)
super

@items = items
@class_name = class_name
end

private

def list_title
LIST_TITLE[@class_name]
end

def empty_list_text
EMPTY_LIST_TEXTS[@class_name]
end
end
21 changes: 21 additions & 0 deletions src/api/app/components/watchlist_component.html.haml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
.navbar-collapse.watchlist-collapse.navbar-dark
.navbar-nav.mb-4
.nav.justify-content-end.py-2
%button.navbar-toggler{ type: 'button', data: { toggle: 'watchlist' }, aria: { expanded: 'false', label: 'Toggle navigation' } }
%i.fas.fa-times

- if @object_to_be_watched
.nav-item.pb-2.mb-4.border-bottom.border-gray-500
= link_to(toggle_watchable_path, method: :put, class: 'nav-link') do
- if object_to_be_watched_in_watchlist?
%p.mb-0.text-light
%i.fas.fa-times-circle
%span= remove_from_watchlist_text
- else
%p.mb-0.text-light
%i.fas.fa-plus-circle
%span= add_to_watchlist_text

= render WatchedItemsListComponent.new(items: projects, class_name: 'Project')
= render WatchedItemsListComponent.new(items: packages, class_name: 'Package')
= render WatchedItemsListComponent.new(items: bs_requests, class_name: 'BsRequest')
58 changes: 58 additions & 0 deletions src/api/app/components/watchlist_component.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
class WatchlistComponent < ApplicationComponent
REMOVE_FROM_WATCHLIST_TEXT = {
'Package' => 'Remove this package from Watchlist',
'Project' => 'Remove this project from Watchlist',
'BsRequest' => 'Remove this request from Watchlist'
}.freeze

ADD_TO_WATCHLIST_TEXT = {
'Package' => 'Watch this package',
'Project' => 'Watch this project',
'BsRequest' => 'Watch this request'
}.freeze

def initialize(user:, project: nil, package: nil, bs_request: nil)
super

@user = user
# NOTE: the order of the array is important, when project and packge are both present we ensure it takes package.
@object_to_be_watched = [bs_request, package, project].compact.first
end

private

def object_to_be_watched_in_watchlist?
!!@user.watched_items.includes(:watchable).find_by(watchable: @object_to_be_watched)
end

def add_to_watchlist_text
ADD_TO_WATCHLIST_TEXT[@object_to_be_watched.class.name]
end

def remove_from_watchlist_text
REMOVE_FROM_WATCHLIST_TEXT[@object_to_be_watched.class.name]
end

def toggle_watchable_path
case @object_to_be_watched
when Package
toggle_package_watched_items_path(project: @object_to_be_watched.project.name, package: @object_to_be_watched.name)
when Project
toggle_project_watched_items_path(project: @object_to_be_watched.name)
when BsRequest
toggle_request_watched_items_path(number: @object_to_be_watched.number)
end
end

def projects
@projects ||= Project.joins(:watched_items).where(watched_items: { user: @user })
end

def packages
@packages ||= Package.joins(:watched_items).where(watched_items: { user: @user })
end

def bs_requests
@bs_requests ||= BsRequest.joins(:watched_items).where(watched_items: { user: @user })
end
end
41 changes: 41 additions & 0 deletions src/api/app/controllers/webui/watched_items_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
class Webui::WatchedItemsController < Webui::WebuiController
before_action :require_login
before_action :check_user_belongs_feature_flag
before_action :set_item

FLASH_PER_WATCHABLE_TYPE = {
Package => 'package',
Project => 'project',
BsRequest => 'request'
}.freeze

def toggle
watched_item = User.session!.watched_items.find_by(watchable: @item)

if watched_item
watched_item.destroy
flash[:success] = "Removed #{FLASH_PER_WATCHABLE_TYPE[@item.class]} from the watchlist"
else
User.session!.watched_items.create(watchable: @item)
flash[:success] = "Added #{FLASH_PER_WATCHABLE_TYPE[@item.class]} to the watchlist"
end

redirect_back(fallback_location: root_path)
end

private

def set_item
@item = if params[:package]
Package.find_by_project_and_name(params[:project], params[:package])
elsif params[:project]
Project.find_by(name: params[:project])
elsif params[:number]
BsRequest.find_by(number: params[:number])
end
end

def check_user_belongs_feature_flag
raise NotFoundError unless Flipper.enabled?(:new_watchlist, User.session)
end
end
1 change: 1 addition & 0 deletions src/api/app/models/bs_request.rb
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ class BsRequest < ApplicationRecord
has_one :request_exclusion, class_name: 'Staging::RequestExclusion', dependent: :destroy
has_many :not_accepted_reviews, -> { where.not(state: :accepted) }, class_name: 'Review'
has_many :notifications, as: :notifiable, dependent: :delete_all
has_many :watched_items, as: :watchable, dependent: :destroy

validates :state, inclusion: { in: VALID_REQUEST_STATES }
validates :creator, presence: true
Expand Down
2 changes: 2 additions & 0 deletions src/api/app/models/package.rb
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@ class Package < ApplicationRecord

after_rollback :reset_cache

has_many :watched_items, as: :watchable, dependent: :destroy

# The default scope is necessary to exclude the forbidden projects.
# It's necessary to write it as a nested Active Record query for performance reasons
# which will produce a query like:
Expand Down
2 changes: 2 additions & 0 deletions src/api/app/models/project.rb
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ def autocomplete(search)
has_many :linked_repositories, through: :path_elements, source: :link, foreign_key: :repository_id
has_many :repository_architectures, -> { order('position') }, through: :repositories

has_many :watched_items, as: :watchable, dependent: :destroy
# FIXME: We will remove the following association when new_watchlist goes out of beta
has_many :watched_projects, dependent: :destroy, inverse_of: :project

has_many :flags, dependent: :delete_all, inverse_of: :project
Expand Down
2 changes: 2 additions & 0 deletions src/api/app/models/user.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ class User < ApplicationRecord
# password yet. this is for backwards compatibility
has_secure_password validations: false

has_many :watched_items, dependent: :destroy
# FIXME: We will remove the following association when new_watchlist goes out of beta
has_many :watched_projects, dependent: :destroy, inverse_of: :user
has_many :groups_users, inverse_of: :user
has_many :roles_users, inverse_of: :user
Expand Down
24 changes: 24 additions & 0 deletions src/api/app/models/watched_item.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
class WatchedItem < ApplicationRecord
belongs_to :watchable, polymorphic: true
belongs_to :user

validates :watchable_id, uniqueness: { scope: [:watchable_type, :user_id] }
end

# == Schema Information
#
# Table name: watched_items
#
# id :integer not null, primary key
# watchable_type :string(255) not null, indexed => [watchable_id, user_id], indexed => [watchable_id]
# created_at :datetime not null
# updated_at :datetime not null
# user_id :integer indexed => [watchable_type, watchable_id], indexed
# watchable_id :integer not null, indexed => [watchable_type, user_id], indexed => [watchable_type]
#
# Indexes
#
# index_watched_items_on_type_id_and_user_id (watchable_type,watchable_id,user_id) UNIQUE
# index_watched_items_on_user_id (user_id)
# index_watched_items_on_watchable (watchable_type,watchable_id)
#
8 changes: 7 additions & 1 deletion src/api/app/views/layouts/webui/webui.html.haml
Original file line number Diff line number Diff line change
Expand Up @@ -76,4 +76,10 @@

-# Collapsible menu shared between top and bottom navigation
- if User.session
= render partial: 'layouts/webui/watchlist'
- if feature_enabled?(:new_watchlist)
= render WatchlistComponent.new(user: User.session!,
project: @project,
package: @package,
bs_request: @bs_request)
- else
= render partial: 'layouts/webui/watchlist'
6 changes: 6 additions & 0 deletions src/api/config/routes/webui_routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -340,6 +340,12 @@
resources :workflow_runs, only: [:index, :show], controller: 'webui/workflow_runs'
end
resources :token_triggers, only: [:show, :update], controller: 'webui/users/token_triggers'

resource :watched_items, controller: 'webui/watched_items', only: [:toggle], constraints: cons do
put '/package/:project/:package/toggle' => :toggle, as: :toggle_package
put '/project/:project/toggle' => :toggle, as: :toggle_project
put '/request/:number/toggle' => :toggle, as: :toggle_request
end
end

get 'home', to: 'webui/webui#home', as: :home
Expand Down
13 changes: 13 additions & 0 deletions src/api/db/migrate/20220210154407_create_watched_items.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
class CreateWatchedItems < ActiveRecord::Migration[6.1]
def change
create_table :watched_items, id: :integer do |t|
t.integer :watchable_id, null: false
t.string :watchable_type, null: false
t.integer :user_id, index: true
t.timestamps
end

add_index :watched_items, [:watchable_type, :watchable_id], name: 'index_watched_items_on_watchable'
add_index :watched_items, [:watchable_type, :watchable_id, :user_id], name: 'index_watched_items_on_type_id_and_user_id', unique: true
end
end
13 changes: 12 additions & 1 deletion src/api/db/schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.

ActiveRecord::Schema.define(version: 2022_01_26_155601) do
ActiveRecord::Schema.define(version: 2022_02_10_154407) do

create_table "architectures", id: :integer, charset: "utf8mb4", collation: "utf8mb4_unicode_ci", options: "ENGINE=InnoDB ROW_FORMAT=DYNAMIC", force: :cascade do |t|
t.string "name", null: false, collation: "utf8_general_ci"
Expand Down Expand Up @@ -1051,6 +1051,17 @@
t.index ["login"], name: "users_login_index", unique: true, length: 255
end

create_table "watched_items", id: :integer, charset: "utf8mb4", collation: "utf8mb4_unicode_ci", force: :cascade do |t|
t.integer "watchable_id", null: false
t.string "watchable_type", null: false
t.integer "user_id"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
t.index ["user_id"], name: "index_watched_items_on_user_id"
t.index ["watchable_type", "watchable_id", "user_id"], name: "index_watched_items_on_type_id_and_user_id", unique: true
t.index ["watchable_type", "watchable_id"], name: "index_watched_items_on_watchable"
end

create_table "watched_projects", id: :integer, charset: "utf8mb4", collation: "utf8mb4_unicode_ci", options: "ENGINE=InnoDB ROW_FORMAT=DYNAMIC", force: :cascade do |t|
t.integer "user_id", default: 0, null: false
t.integer "project_id", null: false
Expand Down
2 changes: 1 addition & 1 deletion src/api/lib/tasks/dev.rake
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
require 'fileutils'
require 'yaml'

ENABLED_FEATURE_FLAGS = [:notifications_redesign, :trigger_workflow].freeze
ENABLED_FEATURE_FLAGS = [:notifications_redesign, :trigger_workflow, :new_watchlist].freeze

namespace :dev do
task :prepare do
Expand Down
Loading

0 comments on commit 6b32cc4

Please sign in to comment.