diff --git a/app/controllers/bubbles_controller.rb b/app/controllers/bubbles_controller.rb index 1b9591008..7765be897 100644 --- a/app/controllers/bubbles_controller.rb +++ b/app/controllers/bubbles_controller.rb @@ -11,10 +11,6 @@ class BubblesController < ApplicationController @bubbles = @bubbles.mentioning(params[:term]) if params[:term].present? end - def new - @bubble = @bucket.bubbles.build - end - def create @bubble = @bucket.bubbles.create! redirect_to @bubble diff --git a/app/controllers/filters_controller.rb b/app/controllers/filters_controller.rb index cc5ee531d..9d9964e52 100644 --- a/app/controllers/filters_controller.rb +++ b/app/controllers/filters_controller.rb @@ -1,8 +1,8 @@ class FiltersController < ApplicationController - before_action :set_filter, only: :destroy + before_action :set_filter, :remember_params, only: :destroy def create - @filter = Current.user.filters.create_or_find_by_params!(filter_params).tap(&:touch) + @filter = Current.user.filters.idempotent_create!(filter_params).tap(&:touch) redirect_to bubbles_path(@filter.to_params) end @@ -16,6 +16,10 @@ class FiltersController < ApplicationController @filter = Current.user.filters.find params[:id] end + def remember_params + @filter_params = @filter.to_params + end + def filter_params params.permit(*Filter::KNOWN_PARAMS).compact_blank end @@ -24,7 +28,7 @@ class FiltersController < ApplicationController if request.referer == root_url redirect_to root_path else - redirect_to bubbles_path(@filter.to_params) + redirect_to bubbles_path(@filter_params) end end end diff --git a/app/helpers/filters_helper.rb b/app/helpers/filters_helper.rb index 677e43fc0..05caac3d0 100644 --- a/app/helpers/filters_helper.rb +++ b/app/helpers/filters_helper.rb @@ -1,9 +1,4 @@ module FiltersHelper - def bubble_filters_heading(filter, &) - tag.h1 class: "txt-large flex align-center gap-half", - style: token_list("margin-inline-end: calc(var(--btn-size) / -2);": filter.savable?), & - end - def buckets_filter_text(filter) if filter.buckets.present? filter.buckets.map(&:name).to_choice_sentence diff --git a/app/models/concerns/filterable.rb b/app/models/concerns/filterable.rb index 81656b42c..b3cb07c46 100644 --- a/app/models/concerns/filterable.rb +++ b/app/models/concerns/filterable.rb @@ -10,8 +10,6 @@ module Filterable private def remove_from_filters - filters.each do |filter| - filter.resource_removed kind: self.class.name.downcase, id: id - end + filters.each { |filter| filter.resource_removed self } end end diff --git a/app/models/filter.rb b/app/models/filter.rb index 9c012911a..7ff26f981 100644 --- a/app/models/filter.rb +++ b/app/models/filter.rb @@ -1,34 +1,17 @@ class Filter < ApplicationRecord - include Summarized - - KNOWN_PARAMS = [ :indexed_by, :assignments, bucket_ids: [], assignee_ids: [], tag_ids: [] ] - INDEXES = %w[ most_active most_discussed most_boosted newest oldest popped ] + include Params, Resources, Summarized belongs_to :creator, class_name: "User", default: -> { Current.user } has_one :account, through: :creator - has_and_belongs_to_many :tags - has_and_belongs_to_many :buckets - has_and_belongs_to_many :assignees, class_name: "User", join_table: "assignees_filters", association_foreign_key: "assignee_id" - - before_validation :denormalize_resource_ids - before_validation :remove_default_params - - store_accessor :params, :indexed_by - store_accessor :params, :assignments - class << self - def default_params - { "indexed_by" => "most_active" } - end - - def create_or_find_by_params!(params) - filter = new params + def idempotent_create!(attrs) + filter = new(attrs) filter.save! filter rescue ActiveRecord::RecordNotUnique - find_by! params: filter.params + find_by! params: filter.params # possible thanks to denormalized params end end @@ -37,33 +20,13 @@ class Filter < ApplicationRecord result = creator.accessible_bubbles.indexed_by(indexed_by) result = result.active unless indexed_by.popped? result = result.unassigned if assignments.unassigned? + result = result.assigned_to(assignees.ids) if assignees.present? result = result.in_bucket(buckets.ids) if buckets.present? result = result.tagged_with(tags.ids) if tags.present? - result = result.assigned_to(assignees.ids) if assignees.present? result end end - def to_params - params.merge(tag_ids: tags.ids, assignee_ids: assignees.ids, bucket_ids: buckets.ids).then do |params| - ActionController::Parameters.new(params).permit(*KNOWN_PARAMS).tap do |params| - params[:filter_id] = id if persisted? - end - end - end - - def indexed_by - (params["indexed_by"] || self.class.default_params["indexed_by"]).inquiry - end - - def assignments - params["assignments"].to_s.inquiry - end - - def savable? - !bucket_default? - end - def cacheable? buckets.exists? end @@ -71,29 +34,4 @@ class Filter < ApplicationRecord def cache_key ActiveSupport::Cache.expand_cache_key buckets.cache_key_with_version, super end - - def resource_removed(kind:, id:) - params["#{kind}_ids"] = Array(params["#{kind}_ids"]).without(id).presence - non_default_params.blank? ? destroy : touch - end - - private - def remove_default_params - self[:params] = non_default_params.compact_blank - end - - def non_default_params - params.reject { |k, v| self.class.default_params[k] == v } - end - - # `denormalize_resource_ids` stores ids in the params column to enforce uniqueness - def denormalize_resource_ids - params[:bucket_ids] = buckets.ids.presence - params[:tag_ids] = tags.ids.presence - params[:assignee_ids] = assignees.ids.presence - end - - def bucket_default? - non_default_params.keys == %w[ bucket_ids ] && buckets.one? - end end diff --git a/app/models/filter/params.rb b/app/models/filter/params.rb new file mode 100644 index 000000000..df2354ebd --- /dev/null +++ b/app/models/filter/params.rb @@ -0,0 +1,51 @@ +module Filter::Params + extend ActiveSupport::Concern + + KNOWN_PARAMS = [ :indexed_by, :assignments, bucket_ids: [], assignee_ids: [], tag_ids: [] ] + INDEXES = %w[ most_active most_discussed most_boosted newest oldest popped ] + + class_methods do + def default_params + { "indexed_by" => "most_active" } + end + end + + included do + before_validation :sanitize_params + end + + def to_params + params.merge(tag_ids: tags.ids.presence, assignee_ids: assignees.ids.presence, bucket_ids: buckets.ids.presence).then do |params| + ActionController::Parameters.new(params).permit(*KNOWN_PARAMS).tap do |params| + params[:filter_id] = id if persisted? + end + end + end + + def assignments=(value) + params["assignments"] = value + end + + def assignments + params["assignments"].to_s.inquiry + end + + def indexed_by=(value) + params["indexed_by"] = value + end + + def indexed_by + (params["indexed_by"] || self.class.default_params["indexed_by"]).inquiry + end + + private + def sanitize_params + denormalize_resource_ids + self.params = non_default_params + params.compact_blank! + end + + def non_default_params + params.reject { |k, v| self.class.default_params[k] == v } + end +end diff --git a/app/models/filter/resources.rb b/app/models/filter/resources.rb new file mode 100644 index 000000000..6f04ded0a --- /dev/null +++ b/app/models/filter/resources.rb @@ -0,0 +1,24 @@ +module Filter::Resources + extend ActiveSupport::Concern + + included do + has_and_belongs_to_many :tags + has_and_belongs_to_many :buckets + has_and_belongs_to_many :assignees, class_name: "User", join_table: "assignees_filters", association_foreign_key: "assignee_id" + end + + def resource_removed(resource) + kind = resource.class.model_name.plural + send("#{kind}=", send(kind).without(resource)) + sanitize_params + params.blank? ? destroy! : save! + end + + private + # `denormalize_resource_ids` stores ids in the params column to enforce uniqueness with a db constraint. + def denormalize_resource_ids + params["bucket_ids"] = buckets.ids + params["tag_ids"] = tags.ids + params["assignee_ids"] = assignees.ids + end +end diff --git a/app/views/bubbles/_filters.html.erb b/app/views/bubbles/_filters.html.erb index 0903cdda3..2efc451c8 100644 --- a/app/views/bubbles/_filters.html.erb +++ b/app/views/bubbles/_filters.html.erb @@ -1,11 +1,11 @@