Many-to-many relationship for filters and filterables
This commit is contained in:
@@ -34,7 +34,7 @@ class BubblesController < ApplicationController
|
||||
|
||||
private
|
||||
def set_filter
|
||||
@filter = Current.user.filters.build params: params.permit(*Filter::KNOWN_PARAMS)
|
||||
@filter = Current.user.filters.build params.permit(*Filter::KNOWN_PARAMS)
|
||||
end
|
||||
|
||||
def set_bubble
|
||||
|
||||
@@ -12,7 +12,7 @@ class BucketsController < ApplicationController
|
||||
|
||||
def create
|
||||
@bucket = Current.account.buckets.create! bucket_params
|
||||
redirect_to bubbles_path(bucket_ids: @bucket)
|
||||
redirect_to bubbles_path(bucket_ids: [ @bucket ])
|
||||
end
|
||||
|
||||
def edit
|
||||
@@ -24,7 +24,7 @@ class BucketsController < ApplicationController
|
||||
@bucket.update! bucket_params
|
||||
@bucket.accesses.revise granted: grantees, revoked: revokees
|
||||
|
||||
redirect_to bubbles_path(bucket_ids: @bucket)
|
||||
redirect_to bubbles_path(bucket_ids: [ @bucket ])
|
||||
end
|
||||
|
||||
def destroy
|
||||
|
||||
@@ -2,7 +2,7 @@ class FiltersController < ApplicationController
|
||||
before_action :set_filter, only: :destroy
|
||||
|
||||
def create
|
||||
@filter = Current.user.filters.create_or_find_by!(params: filter_params).tap(&:touch)
|
||||
@filter = Current.user.filters.create_or_find_by_params!(filter_params).tap(&:touch)
|
||||
redirect_to bubbles_path(@filter.to_params)
|
||||
end
|
||||
|
||||
|
||||
@@ -5,8 +5,8 @@ module FiltersHelper
|
||||
end
|
||||
|
||||
def buckets_filter_text(filter)
|
||||
if filter.buckets.any?
|
||||
filter.buckets.pluck(:name).to_choice_sentence
|
||||
if filter.buckets.present?
|
||||
filter.buckets.map(&:name).to_choice_sentence
|
||||
else
|
||||
"all projects"
|
||||
end
|
||||
@@ -14,7 +14,7 @@ module FiltersHelper
|
||||
|
||||
def assignments_filter_text(filter)
|
||||
if filter.assignees.present?
|
||||
"assigned to #{filter.assignees.pluck(:name).to_choice_sentence}"
|
||||
"assigned to #{filter.assignees.map(&:name).to_choice_sentence}"
|
||||
elsif filter.assignments.unassigned?
|
||||
"assigned to no one"
|
||||
else
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
class Bucket < ApplicationRecord
|
||||
include Filterable
|
||||
include Accessible
|
||||
|
||||
belongs_to :account
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
module Filterable
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
has_and_belongs_to_many :filters
|
||||
|
||||
after_update { filters.touch_all }
|
||||
before_destroy :remove_from_filters
|
||||
end
|
||||
|
||||
private
|
||||
def remove_from_filters
|
||||
filters.each do |filter|
|
||||
filter.resource_removed kind: self.class.name.downcase, id: id
|
||||
end
|
||||
end
|
||||
end
|
||||
+55
-9
@@ -1,35 +1,65 @@
|
||||
class Filter < ApplicationRecord
|
||||
include Assignments, Buckets, Indexes, Summarized, Tags
|
||||
include Summarized
|
||||
|
||||
KNOWN_PARAMS = %i[ indexed_by bucket_ids assignments tag_ids ]
|
||||
KNOWN_PARAMS = [ :indexed_by, :assignments, bucket_ids: [], assignee_ids: [], tag_ids: [] ]
|
||||
INDEXES = %w[ most_active most_discussed most_boosted newest oldest popped ]
|
||||
|
||||
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
|
||||
filter.save!
|
||||
filter
|
||||
rescue ActiveRecord::RecordNotUnique
|
||||
find_by! params: filter.params
|
||||
end
|
||||
end
|
||||
|
||||
def bubbles
|
||||
@bubbles ||= begin
|
||||
result = creator.accessible_bubbles.indexed_by(indexed_by)
|
||||
result = result.active unless indexed_by.popped?
|
||||
result = result.in_bucket(buckets) if buckets.present?
|
||||
result = result.tagged_with(tags) if tags.present?
|
||||
result = result.unassigned if assignments.unassigned?
|
||||
result = result.assigned_to(assignees) 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
|
||||
ActionController::Parameters.new(params).permit(*KNOWN_PARAMS).tap do |params|
|
||||
params[:filter_id] = id if persisted?
|
||||
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
|
||||
@@ -42,12 +72,28 @@ class Filter < ApplicationRecord
|
||||
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 bucket_default?
|
||||
non_default_params.keys == %w[ bucket_ids ] && buckets.one?
|
||||
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
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
module Filter::Assignments
|
||||
def assignments
|
||||
params["assignments"].to_s.inquiry
|
||||
end
|
||||
|
||||
def assignees
|
||||
@assignees ||= account.users.where id: assignments.split(",")
|
||||
end
|
||||
end
|
||||
@@ -1,11 +0,0 @@
|
||||
module Filter::Buckets
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
store_accessor :params, :bucket_ids
|
||||
end
|
||||
|
||||
def buckets
|
||||
@buckets ||= account.buckets.where id: bucket_ids.to_s.split(",")
|
||||
end
|
||||
end
|
||||
@@ -1,7 +0,0 @@
|
||||
module Filter::Indexes
|
||||
INDEXES = %w[ most_active most_discussed most_boosted newest oldest popped ]
|
||||
|
||||
def indexed_by
|
||||
(params["indexed_by"] || self.class.default_params["indexed_by"]).inquiry
|
||||
end
|
||||
end
|
||||
@@ -1,11 +0,0 @@
|
||||
module Filter::Tags
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
store_accessor :params, :tag_ids
|
||||
end
|
||||
|
||||
def tags
|
||||
@tags ||= account.tags.where id: tag_ids.to_s.split(",")
|
||||
end
|
||||
end
|
||||
@@ -1,4 +1,6 @@
|
||||
class Tag < ApplicationRecord
|
||||
include Filterable
|
||||
|
||||
belongs_to :account
|
||||
|
||||
has_many :taggings, dependent: :destroy
|
||||
|
||||
@@ -5,16 +5,16 @@
|
||||
|
||||
<dialog class="filter__popup panel fill-white shadow" data-dialog-target="dialog" data-turbo-temporary>
|
||||
<menu class="filter__menu unpad margin-none">
|
||||
<li><%= link_to "No one", bubbles_path(filter.to_params.merge(assignments: :unassigned)), class: "filter__button" %></li>
|
||||
<li><%= link_to "No one", bubbles_path(filter.to_params.merge(assignments: :unassigned, assignee_ids: [])), class: "filter__button" %></li>
|
||||
|
||||
<% Current.account.users.active.order(:name).each do |user| %>
|
||||
<li><%= link_to user.name, bubbles_path(filter.to_params.merge(assignments: user.id)) %></li>
|
||||
<li><%= link_to user.name, bubbles_path(filter.to_params.merge(assignments: nil, assignee_ids: [ user.id ])) %></li>
|
||||
<% end %>
|
||||
</menu>
|
||||
</dialog>
|
||||
|
||||
<% if filter.assignments.present? %>
|
||||
<%= link_to bubbles_path(filter.to_params.without(:assignments)), class: "btn", style: "font-size: 0.4em;" do %>
|
||||
<% if [ filter.assignees, filter.assignments ].any?(&:present?) %>
|
||||
<%= link_to bubbles_path(filter.to_params.without(:assignments, :assignee_ids)), class: "btn", style: "font-size: 0.4em;" do %>
|
||||
<%= image_tag "remove.svg", aria: { hidden: true }, size: 24 %>
|
||||
<span class="for-screen-reader">Clear</span>
|
||||
<% end %>
|
||||
|
||||
@@ -5,9 +5,27 @@
|
||||
<% end %>
|
||||
<% else %>
|
||||
<%= form_with url: filters_path, method: :post do %>
|
||||
<% Filter::KNOWN_PARAMS.each do |param| %>
|
||||
<%= hidden_field_tag param, params[param] if params[param].present? %>
|
||||
<%= hidden_field_tag :indexed_by, params[:indexed_by] if params[:indexed_by].present? %>
|
||||
<%= hidden_field_tag :assignments, params[:assignments] if params[:assignments].present? %>
|
||||
|
||||
<% if values = params[:bucket_ids].presence %>
|
||||
<% values.each do |value| %>
|
||||
<%= hidden_field_tag "bucket_ids[]", value, id: nil %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
<% if values = params[:tag_ids].presence %>
|
||||
<% values.each do |value| %>
|
||||
<%= hidden_field_tag "tag_ids[]", value, id: nil %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
<% if values = params[:assignee_ids].presence %>
|
||||
<% values.each do |value| %>
|
||||
<%= hidden_field_tag "assignee_ids[]", value, id: nil %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
<%= button_tag type: :submit, class: "btn txt-small borderless" do %>
|
||||
<%= image_tag "bookmark-outline.svg", aria: { hidden: true }, size: 24 %>
|
||||
<span class="for-screen-reader">Save this filter</span>
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<li><%= link_to "all projects", bubbles_path(filter.to_params.merge(bucket_ids: nil)), class: "filter__button" %></li>
|
||||
|
||||
<% Current.user.buckets.each do |bucket| %>
|
||||
<li><%= link_to bucket.name, bubbles_path(filter.to_params.merge(bucket_ids: bucket.id)), class: "filter__button" %></li>
|
||||
<li><%= link_to bucket.name, bubbles_path(filter.to_params.merge(bucket_ids: [ bucket.id ])), class: "filter__button" %></li>
|
||||
<% end %>
|
||||
</menu>
|
||||
</dialog>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<dialog class="filter__popup panel fill-white shadow" data-dialog-target="dialog" data-turbo-temporary>
|
||||
<menu class="filter__menu unpad margin-none">
|
||||
<% Current.account.tags.order(:title).each do |tag| %>
|
||||
<li><%= link_to tag.title, bubbles_path(filter.to_params.merge(tag_ids: tag.id)) %></li>
|
||||
<li><%= link_to tag.title, bubbles_path(filter.to_params.merge(tag_ids: [ tag.id ])) %></li>
|
||||
<% end %>
|
||||
</menu>
|
||||
</dialog>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<% cache bucket do %>
|
||||
<li class="bucket flex flex-column txt-align-center max-width position-relative">
|
||||
<%= link_to bubbles_path(bucket_ids: bucket), class: "windshield__container flex justify-center align-center position-relative" do %>
|
||||
<%= link_to bubbles_path(bucket_ids: [ bucket ]), class: "windshield__container flex justify-center align-center position-relative" do %>
|
||||
<div class="windshield bucket__windshield flex flex-wrap gap justify-center align-end" style="view-transition-name: windshield_<%= bucket.id %>">
|
||||
<% bucket.bubbles.ordered_by_activity.limit(10).each do |bubble| %>
|
||||
<div class="bubble" style="--bubble-color: <%= bubble.color %>; <%= bubble_rotation(bubble) %> <%= bubble_size(bubble) %>">
|
||||
@@ -14,7 +14,7 @@
|
||||
<% end %>
|
||||
|
||||
<div class="flex align-center justify-center flex-column flex-wrap center gap-half">
|
||||
<%= link_to bubbles_path(bucket_ids: bucket), class: "txt-ink flex flex-column" do %>
|
||||
<%= link_to bubbles_path(bucket_ids: [ bucket ]), class: "txt-ink flex flex-column" do %>
|
||||
<strong class="txt-x-large">In <%= bucket.name %></strong>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
class CreateFilterJoinTables < ActiveRecord::Migration[8.0]
|
||||
def change
|
||||
create_join_table :filters, :tags do |t|
|
||||
t.index :filter_id
|
||||
t.index :tag_id
|
||||
end
|
||||
|
||||
create_join_table :filters, :buckets do |t|
|
||||
t.index :filter_id
|
||||
t.index :bucket_id
|
||||
end
|
||||
|
||||
create_join_table :filters, :assignees do |t|
|
||||
t.index :filter_id
|
||||
t.index :assignee_id
|
||||
end
|
||||
end
|
||||
end
|
||||
Generated
+22
-1
@@ -10,7 +10,7 @@
|
||||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema[8.0].define(version: 2024_11_05_181312) do
|
||||
ActiveRecord::Schema[8.0].define(version: 2024_11_05_224305) do
|
||||
create_table "accesses", force: :cascade do |t|
|
||||
t.integer "bucket_id", null: false
|
||||
t.integer "user_id", null: false
|
||||
@@ -58,6 +58,13 @@ ActiveRecord::Schema[8.0].define(version: 2024_11_05_181312) do
|
||||
t.index ["blob_id", "variation_digest"], name: "index_active_storage_variant_records_uniqueness", unique: true
|
||||
end
|
||||
|
||||
create_table "assignees_filters", id: false, force: :cascade do |t|
|
||||
t.integer "filter_id", null: false
|
||||
t.integer "assignee_id", null: false
|
||||
t.index ["assignee_id"], name: "index_assignees_filters_on_assignee_id"
|
||||
t.index ["filter_id"], name: "index_assignees_filters_on_filter_id"
|
||||
end
|
||||
|
||||
create_table "assignments", force: :cascade do |t|
|
||||
t.integer "assignee_id", null: false
|
||||
t.integer "bubble_id", null: false
|
||||
@@ -92,6 +99,13 @@ ActiveRecord::Schema[8.0].define(version: 2024_11_05_181312) do
|
||||
t.index ["creator_id"], name: "index_buckets_on_creator_id"
|
||||
end
|
||||
|
||||
create_table "buckets_filters", id: false, force: :cascade do |t|
|
||||
t.integer "filter_id", null: false
|
||||
t.integer "bucket_id", null: false
|
||||
t.index ["bucket_id"], name: "index_buckets_filters_on_bucket_id"
|
||||
t.index ["filter_id"], name: "index_buckets_filters_on_filter_id"
|
||||
end
|
||||
|
||||
create_table "comments", force: :cascade do |t|
|
||||
t.text "body", null: false
|
||||
t.integer "creator_id", null: false
|
||||
@@ -123,6 +137,13 @@ ActiveRecord::Schema[8.0].define(version: 2024_11_05_181312) do
|
||||
t.index ["creator_id", "params"], name: "index_filters_on_creator_id_and_params", unique: true
|
||||
end
|
||||
|
||||
create_table "filters_tags", id: false, force: :cascade do |t|
|
||||
t.integer "filter_id", null: false
|
||||
t.integer "tag_id", null: false
|
||||
t.index ["filter_id"], name: "index_filters_tags_on_filter_id"
|
||||
t.index ["tag_id"], name: "index_filters_tags_on_tag_id"
|
||||
end
|
||||
|
||||
create_table "messages", force: :cascade do |t|
|
||||
t.integer "bubble_id", null: false
|
||||
t.string "messageable_type", null: false
|
||||
|
||||
@@ -21,7 +21,7 @@ class BucketsControllerTest < ActionDispatch::IntegrationTest
|
||||
end
|
||||
|
||||
bucket = Bucket.last
|
||||
assert_redirected_to bubbles_path(bucket_ids: bucket.id)
|
||||
assert_redirected_to bubbles_path(bucket_ids: [ bucket ])
|
||||
assert_includes bucket.users, users(:kevin)
|
||||
assert_equal "Remodel Punch List", bucket.name
|
||||
end
|
||||
@@ -34,7 +34,7 @@ class BucketsControllerTest < ActionDispatch::IntegrationTest
|
||||
test "update" do
|
||||
patch bucket_url(buckets(:writebook)), params: { bucket: { name: "Writebook bugs" }, user_ids: users(:david, :jz).pluck(:id) }
|
||||
|
||||
assert_redirected_to bubbles_path(bucket_ids: buckets(:writebook))
|
||||
assert_redirected_to bubbles_path(bucket_ids: [ buckets(:writebook) ])
|
||||
assert_equal "Writebook bugs", buckets(:writebook).reload.name
|
||||
assert_equal users(:david, :jz), buckets(:writebook).users
|
||||
end
|
||||
|
||||
Vendored
+3
-1
@@ -1,3 +1,5 @@
|
||||
jz_assignments:
|
||||
creator: david
|
||||
params: <%= { indexed_by: :most_discussed, assignments: ActiveRecord::FixtureSet.identify(:jz).to_s, tag_ids: ActiveRecord::FixtureSet.identify(:mobile).to_s }.to_json %>
|
||||
params: <%= { indexed_by: :most_discussed }.to_json %>
|
||||
tags: mobile
|
||||
assignees: jz
|
||||
|
||||
Reference in New Issue
Block a user