diff --git a/app/controllers/bubbles_controller.rb b/app/controllers/bubbles_controller.rb index 59fb765b5..3987ac5a5 100644 --- a/app/controllers/bubbles_controller.rb +++ b/app/controllers/bubbles_controller.rb @@ -2,24 +2,15 @@ class BubblesController < ApplicationController include BucketScoped before_action :set_bubble, only: %i[ show edit update ] + before_action :set_assignee_filters, :set_tag_filters, only: :index def index @bubbles = @bucket.bubbles - @bubbles = params.include?(:popped) ? @bubbles.popped : @bubbles.not_popped - - if params[:term].present? - @bubbles = @bubbles.mentioning(params[:term]) - end - - if params[:tag_id] - @tag = Current.account.tags.find(params[:tag_id]) - @bubbles = @bubbles.tagged_with(@tag) - end - - if params[:assignee_id] - @assignee = @bucket.users.find(params[:assignee_id]) - @bubbles = @bubbles.assigned_to(@assignee) - end + @bubbles = @bubbles.ordered_by(params[:order_by] || Bubble.default_order_by) + @bubbles = @bubbles.with_status(params[:status] || Bubble.default_status) + @bubbles = @bubbles.tagged_with(@tag_filters) if @tag_filters + @bubbles = @bubbles.assigned_to(@assignee_filters) if @assignee_filters + @bubbles = @bubbles.mentioning(params[:term]) if params[:term] end def new @@ -38,13 +29,27 @@ class BubblesController < ApplicationController end def update - @bubble.update!(bubble_params) + @bubble.update! bubble_params redirect_to bucket_bubble_url(@bucket, @bubble) end private def set_bubble - @bubble = @bucket.bubbles.find(params[:id]) + @bubble = @bucket.bubbles.find params[:id] + end + + def set_assignee_filters + params[:assignee_ids] = nil if status_filter_param.unassigned? + @assignee_filters = Current.account.users.where(id: params[:assignee_ids]) if params[:assignee_ids] + end + + def status_filter_param + params.fetch(:status, "")&.inquiry + end + helper_method :status_filter_param + + def set_tag_filters + @tag_filters = Current.account.tags.where(id: params[:tag_ids]) if params[:tag_ids] end def bubble_params diff --git a/app/controllers/buckets/views_controller.rb b/app/controllers/buckets/views_controller.rb new file mode 100644 index 000000000..b225cffae --- /dev/null +++ b/app/controllers/buckets/views_controller.rb @@ -0,0 +1,21 @@ +class Buckets::ViewsController < ApplicationController + include BucketScoped + + def create + @bucket.views.create! filters: view_params.merge(assignee_ids:, tag_ids:).compact_blank + redirect_back_or_to bucket_bubbles_path(@bucket), notice: "Filters saved" + end + + private + def view_params + params.require(:view).permit(:order_by, :status, :assignee_ids, :tag_ids) + end + + def assignee_ids + view_params[:assignee_ids]&.split(",") + end + + def tag_ids + view_params[:tag_ids]&.split(",") + end +end diff --git a/app/controllers/buckets_controller.rb b/app/controllers/buckets_controller.rb index 275496794..3c70164f6 100644 --- a/app/controllers/buckets_controller.rb +++ b/app/controllers/buckets_controller.rb @@ -2,7 +2,7 @@ class BucketsController < ApplicationController before_action :set_bucket, except: %i[ index new create ] def index - @buckets = Current.user.buckets.all + @buckets = (Current.user.buckets.all + Current.user.bucket_views.all).sort_by(&:updated_at) end def new @@ -10,7 +10,7 @@ class BucketsController < ApplicationController end def create - @bucket = Current.account.buckets.create!(bucket_params) + @bucket = Current.account.buckets.create! bucket_params redirect_to bucket_bubbles_url(@bucket) end diff --git a/app/helpers/bubbles_helper.rb b/app/helpers/bubbles_helper.rb index 8bdf50648..9cb9bd30d 100644 --- a/app/helpers/bubbles_helper.rb +++ b/app/helpers/bubbles_helper.rb @@ -20,8 +20,4 @@ module BubblesHelper "--bubble-size: var(--bubble-size-#{rank});" end - - def bubble_filter_params - @bubble_filter_params ||= params.permit(:popped, :term, :tag_id, :assignee_id) - end end diff --git a/app/helpers/filters_helper.rb b/app/helpers/filters_helper.rb new file mode 100644 index 000000000..143b19390 --- /dev/null +++ b/app/helpers/filters_helper.rb @@ -0,0 +1,26 @@ +module FiltersHelper + def main_filter_text + (Bucket::View::ORDERS[params[:order_by]] || Bucket::View::STATUSES[params[:status]] || Bubble.default_order_by.humanize).upcase_first + end + + def tag_filter_text + if @tag_filters + @tag_filters.map(&:hashtag).to_choice_sentence + else + "any tag" + end + end + + def assignee_filter_text + if @assignee_filters + "assigned to #{@assignee_filters.map(&:name).to_choice_sentence}" + else + "assigned to anyone" + end + end + + # `#bubble_filter_params` is memoized to avoid spam in logs about unpermitted params + def bubble_filter_params + @bubble_filter_params ||= params.permit :order_by, :status, :term, :assignee_ids, :tag_ids, assignee_ids: [], tag_ids: [] + end +end diff --git a/app/models/bubble.rb b/app/models/bubble.rb index debbf14cd..1e7b4ebef 100644 --- a/app/models/bubble.rb +++ b/app/models/bubble.rb @@ -1,4 +1,5 @@ class Bubble < ApplicationRecord + include ::Searchable include Assignable, Boostable, Colored, Commentable, Eventable, Poppable, Searchable, Taggable, Threaded belongs_to :bucket @@ -10,16 +11,28 @@ class Bubble < ApplicationRecord before_save :set_default_title - scope :reverse_chronologically, -> { order(created_at: :desc, id: :desc) } + scope :reverse_chronologically, -> { order created_at: :desc, id: :desc } + scope :chronologically, -> { order created_at: :asc, id: :asc } scope :ordered_by_activity, -> { left_joins(:comments).group(:id).order(Arel.sql("COUNT(comments.id) + boost_count DESC")) } + scope :with_status, ->(status) { public_send status.presence_in(%w[ popped not_popped unassigned ]) } + scope :ordered_by, ->(order) do + case order + when "most_active" then ordered_by_activity + when "most_discussed" then ordered_by_comments + when "most_boosted" then ordered_by_boosts + when "newest" then reverse_chronologically + when "oldest" then chronologically + end + end - scope :mentioning, ->(query) do - bubbles = search(query).select(:id).to_sql - comments = Comment.search(query).select(:bubble_id).to_sql + class << self + def default_order_by + "most_active" + end - left_joins(:comments) - .where("bubbles.id in (#{bubbles}) or comments.bubble_id in (#{comments})") - .distinct + def default_status + "not_popped" + end end private diff --git a/app/models/bubble/assignable.rb b/app/models/bubble/assignable.rb index 828034dab..db0b405eb 100644 --- a/app/models/bubble/assignable.rb +++ b/app/models/bubble/assignable.rb @@ -7,8 +7,8 @@ module Bubble::Assignable has_many :assignees, through: :assignments has_many :assigners, through: :assignments - scope :assigned_to, ->(user) { joins(:assignments).where(assignments: { assignee: user }) } - scope :assigned_by, ->(user) { joins(:assignments).where(assignments: { assigner: user }) } + scope :unassigned, -> { where.missing :assignments } + scope :assigned_to, ->(users) { joins(:assignments).where(assignments: { assignee: users }) } end def assign(users, assigner: Current.user) diff --git a/app/models/bubble/boostable.rb b/app/models/bubble/boostable.rb index 98ce28485..0853603bc 100644 --- a/app/models/bubble/boostable.rb +++ b/app/models/bubble/boostable.rb @@ -1,6 +1,10 @@ module Bubble::Boostable extend ActiveSupport::Concern + included do + scope :ordered_by_boosts, -> { order boost_count: :desc } + end + def boost! transaction do increment! :boost_count diff --git a/app/models/bubble/commentable.rb b/app/models/bubble/commentable.rb index 911fd3956..7d4dda8c9 100644 --- a/app/models/bubble/commentable.rb +++ b/app/models/bubble/commentable.rb @@ -3,6 +3,8 @@ module Bubble::Commentable included do has_many :comments, dependent: :destroy + + scope :ordered_by_comments, -> { left_joins(:comments).group(:id).order("COUNT(comments.id) DESC") } end def comment!(body) diff --git a/app/models/bubble/poppable.rb b/app/models/bubble/poppable.rb index 30e6cde65..9ac34ec3d 100644 --- a/app/models/bubble/poppable.rb +++ b/app/models/bubble/poppable.rb @@ -4,8 +4,8 @@ module Bubble::Poppable included do has_one :pop, dependent: :destroy - scope :popped, -> { joins(:pop) } - scope :not_popped, -> { where.missing(:pop) } + scope :popped, -> { joins(:pop) } + scope :not_popped, -> { where.missing(:pop) } end def popped? diff --git a/app/models/bubble/searchable.rb b/app/models/bubble/searchable.rb new file mode 100644 index 000000000..dd951899c --- /dev/null +++ b/app/models/bubble/searchable.rb @@ -0,0 +1,14 @@ +module Bubble::Searchable + extend ActiveSupport::Concern + + included do + scope :mentioning, ->(query) do + bubbles = search(query).select(:id).to_sql + comments = Comment.search(query).select(:bubble_id).to_sql + + left_joins(:comments) + .where("bubbles.id in (#{bubbles}) or comments.bubble_id in (#{comments})") + .distinct + end + end +end diff --git a/app/models/bubble/taggable.rb b/app/models/bubble/taggable.rb index 33c53d160..bad321ad0 100644 --- a/app/models/bubble/taggable.rb +++ b/app/models/bubble/taggable.rb @@ -5,6 +5,6 @@ module Bubble::Taggable has_many :taggings, dependent: :destroy has_many :tags, through: :taggings - scope :tagged_with, ->(tag) { joins(:taggings).where(taggings: { tag: tag }) } + scope :tagged_with, ->(tags) { joins(:taggings).where(taggings: { tag: tags }) } end end diff --git a/app/models/bucket.rb b/app/models/bucket.rb index f2476b761..651b8e600 100644 --- a/app/models/bucket.rb +++ b/app/models/bucket.rb @@ -1,5 +1,5 @@ class Bucket < ApplicationRecord - include Accessible + include Accessible, Views belongs_to :account belongs_to :creator, class_name: "User", default: -> { Current.user } diff --git a/app/models/bucket/view.rb b/app/models/bucket/view.rb new file mode 100644 index 000000000..6eed3e940 --- /dev/null +++ b/app/models/bucket/view.rb @@ -0,0 +1,37 @@ +class Bucket::View < ApplicationRecord + FILTERS = [ :assignee_ids, :order_by, :status, :tag_ids ] + + include Summarized, Assignees, Tags + + belongs_to :creator, class_name: "User", default: -> { Current.user } + belongs_to :bucket + + has_one :account, through: :creator + + store_accessor :filters, *FILTERS + + validate :must_have_filters, :must_not_be_the_default_view + + def to_bucket_params + filters.compact_blank + end + + private + ORDERS = { + "most_active" => "most active", + "most_discussed" => "most discussed", + "most_boosted" => "most boosted", + "newest" => "newest", + "oldest" => "oldest" } + STATUSES = { + "unassigned" => "unassigned", + "popped" => "popped" } + + def must_have_filters + errors.add(:base, "must have filters") if filters.values.all?(&:blank?) + end + + def must_not_be_the_default_view + errors.add(:base, "must be different than the default view") if filters.compact_blank == { "order_by" => "most_active" } + end +end diff --git a/app/models/bucket/view/assignees.rb b/app/models/bucket/view/assignees.rb new file mode 100644 index 000000000..fd0aaa5c1 --- /dev/null +++ b/app/models/bucket/view/assignees.rb @@ -0,0 +1,10 @@ +module Bucket::View::Assignees + private + def assignee_names + assignees.map &:name + end + + def assignees + @assignees ||= account.users.where id: assignee_ids + end +end diff --git a/app/models/bucket/view/summarized.rb b/app/models/bucket/view/summarized.rb new file mode 100644 index 000000000..a9c8cf7ed --- /dev/null +++ b/app/models/bucket/view/summarized.rb @@ -0,0 +1,24 @@ +module Bucket::View::Summarized + def summary + [ order_by_summary, status_summary, assignee_summary, tag_summary ].compact.to_sentence.upcase_first + end + + private + delegate :to_sentence, to: "ApplicationController.helpers", private: true + + def order_by_summary + Bucket::View::ORDERS.fetch order_by, nil + end + + def status_summary + Bucket::View::STATUSES.fetch status, nil + end + + def assignee_summary + "assigned to #{assignee_names.to_choice_sentence}" if assignee_names.any? + end + + def tag_summary + "tagged #{tag_names.to_choice_sentence}" if tag_names.any? + end +end diff --git a/app/models/bucket/view/tags.rb b/app/models/bucket/view/tags.rb new file mode 100644 index 000000000..f033c2dc7 --- /dev/null +++ b/app/models/bucket/view/tags.rb @@ -0,0 +1,10 @@ +module Bucket::View::Tags + private + def tag_names + tags.map &:hashtag + end + + def tags + @tags ||= account.tags.where id: tag_ids + end +end diff --git a/app/models/bucket/views.rb b/app/models/bucket/views.rb new file mode 100644 index 000000000..ab9cba2c8 --- /dev/null +++ b/app/models/bucket/views.rb @@ -0,0 +1,7 @@ +module Bucket::Views + extend ActiveSupport::Concern + + included do + has_many :views, dependent: :delete_all + end +end diff --git a/app/models/event.rb b/app/models/event.rb index 8994acc05..76e60ff30 100644 --- a/app/models/event.rb +++ b/app/models/event.rb @@ -6,5 +6,7 @@ class Event < ApplicationRecord belongs_to :creator, class_name: "User" belongs_to :bubble + has_one :account, through: :creator + scope :threadable, -> { where action: THREADABLE_ACTIONS } end diff --git a/app/models/event/assignments.rb b/app/models/event/assignments.rb index ade131d31..1daedbf9f 100644 --- a/app/models/event/assignments.rb +++ b/app/models/event/assignments.rb @@ -11,6 +11,6 @@ module Event::Assignments private def assignees - @assignees ||= creator.account.users.find assignee_ids + @assignees ||= account.users.where id: assignee_ids end end diff --git a/app/models/tag.rb b/app/models/tag.rb index 1e028466f..fb8db226d 100644 --- a/app/models/tag.rb +++ b/app/models/tag.rb @@ -3,4 +3,8 @@ class Tag < ApplicationRecord has_many :taggings, dependent: :destroy has_many :bubbles, through: :taggings + + def hashtag + "#" + title + end end diff --git a/app/models/user.rb b/app/models/user.rb index 2d1c5ce6f..d214b1120 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -1,4 +1,6 @@ class User < ApplicationRecord + include Viewer + belongs_to :account has_many :sessions, dependent: :destroy diff --git a/app/models/user/viewer.rb b/app/models/user/viewer.rb new file mode 100644 index 000000000..a3d59585e --- /dev/null +++ b/app/models/user/viewer.rb @@ -0,0 +1,7 @@ +module User::Viewer + extend ActiveSupport::Concern + + included do + has_many :bucket_views, class_name: "Bucket::View", foreign_key: :creator_id, dependent: :destroy + end +end diff --git a/app/views/bubbles/_filters.html.erb b/app/views/bubbles/_filters.html.erb index a98fe7771..60e4b31e2 100644 --- a/app/views/bubbles/_filters.html.erb +++ b/app/views/bubbles/_filters.html.erb @@ -1,21 +1,18 @@
Nothing here.
<% end %> @@ -37,6 +37,6 @@<%= view.summary %>
+ <% end %> +