Unnest bubble filters

This commit is contained in:
Jose Farias
2024-11-05 13:04:05 -06:00
parent 26af27ba26
commit f2706d0f74
49 changed files with 381 additions and 437 deletions
+8 -16
View File
@@ -1,12 +1,14 @@
class BubblesController < ApplicationController
include BucketScoped
skip_before_action :set_bucket, only: :index
before_action :set_filter, only: :index
before_action :set_bubble, only: %i[ show edit update ]
before_action :clear_assignees, only: :index
before_action :set_view, :set_filter, only: :index
def index
@bubbles = @filter.bubbles
@bubbles = @bubbles.mentioning(params[:term]) if params[:term].present?
end
def new
@@ -31,6 +33,10 @@ class BubblesController < ApplicationController
end
private
def set_filter
@filter = Current.user.filters.build params: params.permit(*Filter::KNOWN_PARAMS)
end
def set_bubble
@bubble = @bucket.bubbles.find params[:id]
end
@@ -38,18 +44,4 @@ class BubblesController < ApplicationController
def bubble_params
params.expect(bubble: [ :title, :color, :due_on, :image, tag_ids: [] ])
end
def clear_assignees
params[:assignee_ids] = nil if helpers.unassigned_filter_activated?
end
def set_view
@view = @bucket.views.find_by(creator: Current.user, id: params[:view_id]) if params[:view_id]
@view ||= @bucket.views.find_by(creator: Current.user, filters: helpers.bubble_filter_params.to_h)
params[:view_id] = @view&.id
end
def set_filter
@filter = @bucket.bubble_filter_from helpers.view_filter_params
end
end
@@ -1,29 +0,0 @@
class Buckets::ViewsController < ApplicationController
include BucketScoped
before_action :set_view, only: %i[ update destroy ]
def create
@view = @bucket.views.create! filters: filter_params
redirect_to bucket_bubbles_path(@bucket, **filter_params.merge(view_id: @view.id)), notice: ""
end
def update
@view.update! filters: filter_params
redirect_to bucket_bubbles_path(@bucket, **filter_params.merge(view_id: @view.id)), notice: ""
end
def destroy
@view.destroy
redirect_to buckets_path
end
private
def set_view
@view = @bucket.views.find_by creator: Current.user, id: params[:id]
end
def filter_params
helpers.bubble_filter_params.to_h
end
end
+4 -3
View File
@@ -2,7 +2,8 @@ class BucketsController < ApplicationController
before_action :set_bucket, except: %i[ index new create ]
def index
@buckets = (Current.user.buckets.all + Current.user.bucket_views.all).sort_by(&:updated_at).reverse!
@filters = Current.user.filters.all
@buckets = Current.user.buckets.all
end
def new
@@ -11,7 +12,7 @@ class BucketsController < ApplicationController
def create
@bucket = Current.account.buckets.create! bucket_params
redirect_to bucket_bubbles_path(@bucket)
redirect_to bubbles_path(bucket_ids: @bucket)
end
def edit
@@ -23,7 +24,7 @@ class BucketsController < ApplicationController
@bucket.update! bucket_params
@bucket.accesses.revise granted: grantees, revoked: revokees
redirect_to bucket_bubbles_path(@bucket)
redirect_to bubbles_path(bucket_ids: @bucket)
end
def destroy
+30
View File
@@ -0,0 +1,30 @@
class FiltersController < ApplicationController
before_action :set_filter, only: :destroy
def create
@filter = Current.user.filters.create_or_find_by!(params: filter_params).tap(&:touch)
redirect_to bubbles_path(@filter.to_params)
end
def destroy
@filter.destroy!
redirect_after_destroy
end
private
def set_filter
@filter = Current.user.filters.find params[:id]
end
def filter_params
params.permit(*Filter::KNOWN_PARAMS).compact_blank
end
def redirect_after_destroy
if request.referer == root_url
redirect_to root_path
else
redirect_to bubbles_path(@filter.to_params)
end
end
end
+16 -45
View File
@@ -1,61 +1,32 @@
module FiltersHelper
def main_filter_text
(Bucket::View::ORDERS[params[:order_by]] || Bucket::View::STATUSES[params[:status]] || Bubble.default_order_by).humanize
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 tag_filter_text(filter)
if filter.tags
filter.tags.map(&:hashtag).to_choice_sentence
def buckets_filter_text(filter)
if filter.buckets.any?
filter.buckets.pluck(:name).to_choice_sentence
else
"any tag"
"all projects"
end
end
def assignee_filter_text(filter)
if filter.assignees
def assignments_filter_text(filter)
if filter.assignees.present?
"assigned to #{filter.assignees.pluck(:name).to_choice_sentence}"
elsif filter.assignments.unassigned?
"assigned to no one"
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, assignee_ids: [], tag_ids: []
end
# `#view_filter_params` is memoized to avoid spam in logs about unpermitted params
def view_filter_params
@view_filter_params ||= bubble_filter_params.merge params.permit(:term, :view_id)
end
def unassigned_filter_activated?
params[:status] == "unassigned"
end
def default_filters?
bubble_filter_params.values.all?(&:blank?) || bubble_filter_params.to_h == Bucket::View.default_filters
end
def bubble_filter_form_tag(path, method:, id: nil)
form_tag path, method: method, id: id do
yield if block_given?
if params[:order_by].present?
concat hidden_field_tag(:order_by, params[:order_by])
end
if params[:status].present?
concat hidden_field_tag(:status, params[:status])
end
Array(params[:assignee_ids]).each do |assignee_id|
concat hidden_field_tag("assignee_ids[]", assignee_id, id: nil)
end
Array(params[:tag_ids]).each do |tag_id|
concat hidden_field_tag("tag_ids[]", tag_id, id: nil)
end
def tags_filter_text(filter)
if filter.tags.present?
filter.tags.map(&:hashtag).to_choice_sentence
else
"any tag"
end
end
end
@@ -0,0 +1,13 @@
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static values = { fallbackDestination: String }
connect() {
if (history.state.turbo?.restorationIndex > 0) {
this.element.href = "javascript:history.back()"
} else {
this.element.href = this.fallbackDestinationValue
}
}
}
@@ -0,0 +1,9 @@
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
merge({ params: { key, value } }) {
const url = new URL(window.location.href)
url.searchParams.set(key, value)
Turbo.visit(url)
}
}
+5 -19
View File
@@ -1,7 +1,7 @@
class Bubble < ApplicationRecord
include Assignable, Boostable, Colored, Eventable, Messages, Poppable, Searchable, Staged, Taggable
belongs_to :bucket
belongs_to :bucket, touch: true
belongs_to :creator, class_name: "User", default: -> { Current.user }
has_one_attached :image, dependent: :purge_later
@@ -10,6 +10,7 @@ class Bubble < ApplicationRecord
scope :reverse_chronologically, -> { order created_at: :desc, id: :desc }
scope :chronologically, -> { order created_at: :asc, id: :asc }
scope :in_bucket, ->(bucket) { where bucket: bucket }
# FIXME: Compute activity and comment count at write time so it's easier to query for.
scope :left_joins_comments, -> do
@@ -22,29 +23,14 @@ class Bubble < ApplicationRecord
left_joins_comments.select("bubbles.*, COUNT(comments.id) AS comment_count").group(:id).order("comment_count DESC")
end
# FIXME: `status` implies an enum. Consider a name change.
scope :with_status, ->(status) do
status = status.presence_in %w[ popped active unassigned ]
public_send(status) if status
end
scope :ordered_by, ->(order) do
case order
scope :indexed_by, ->(index) do
case index
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
class << self
def default_order_by
"most_active"
end
def default_status
"active"
when "popped" then popped
end
end
+3 -5
View File
@@ -7,11 +7,9 @@ module Bubble::Searchable
searchable_by :title, using: :bubbles_search_index
scope :mentioning, ->(query) do
if query = query.presence
bubbles = search(query).select(:id).to_sql
comments = Comment.search(query).select(:id).to_sql
left_joins(:messages).where("bubbles.id in (#{bubbles}) or messages.messageable_id in (#{comments})").distinct
end
bubbles = Bubble.search(query).select(:id).to_sql
comments = Comment.search(query).select(:id).to_sql
left_joins(:messages).where("bubbles.id in (#{bubbles}) or messages.messageable_id in (#{comments})").distinct
end
end
end
+1 -1
View File
@@ -1,5 +1,5 @@
class Bucket < ApplicationRecord
include Accessible, Filterable, Views
include Accessible
belongs_to :account
belongs_to :creator, class_name: "User", default: -> { Current.user }
-34
View File
@@ -1,34 +0,0 @@
class Bucket::BubbleFilter
def initialize(bucket, params = {})
@bucket = bucket
@status = params["status"]
@order_by = params["order_by"]
@term = params["term"]
@tag_ids = params["tag_ids"]
@assignee_ids = params["assignee_ids"]
end
def bubbles
@bubbles ||= begin
result = bucket.bubbles
result = result.ordered_by(order_by || Bubble.default_order_by)
result = result.with_status(status || Bubble.default_status)
result = result.tagged_with(tags) if tags
result = result.assigned_to(assignees) if assignees
result = result.mentioning(term) if term
result
end
end
def tags
@tags ||= account.tags.where(id: tag_ids) if tag_ids
end
def assignees
@assignees ||= account.users.where(id: assignee_ids) if assignee_ids
end
private
attr_reader :bucket, :status, :order_by, :term, :tag_ids, :assignee_ids
delegate :account, to: :bucket, private: true
end
-5
View File
@@ -1,5 +0,0 @@
module Bucket::Filterable
def bubble_filter_from(params = {})
Bucket::BubbleFilter.new self, params
end
end
-27
View File
@@ -1,27 +0,0 @@
class Bucket::View < ApplicationRecord
include Assignees, OrderBy, Status, Summarized, Tags
belongs_to :creator, class_name: "User", default: -> { Current.user }
belongs_to :bucket
has_one :account, through: :creator
validate :must_not_be_the_default_view
class << self
def default_filters
{ "order_by" => "most_active" }
end
end
def bubbles
@bubbles ||= bucket.bubble_filter_from(filters).bubbles
end
private
def must_not_be_the_default_view
if filters.values.all?(&:blank?) || filters == self.class.default_filters
errors.add :base, "must be different than the default view"
end
end
end
-12
View File
@@ -1,12 +0,0 @@
module Bucket::View::Assignees
extend ActiveSupport::Concern
included do
store_accessor :filters, :assignee_ids
end
private
def assignees
@assignees ||= account.users.where id: assignee_ids
end
end
-15
View File
@@ -1,15 +0,0 @@
module Bucket::View::OrderBy
extend ActiveSupport::Concern
included do
store_accessor :filters, :order_by
end
private
ORDERS = {
"most_active" => "most active",
"most_discussed" => "most discussed",
"most_boosted" => "most boosted",
"newest" => "newest",
"oldest" => "oldest" }
end
-10
View File
@@ -1,10 +0,0 @@
module Bucket::View::Status
extend ActiveSupport::Concern
included do
store_accessor :filters, :status
end
private
STATUSES = %w[ unassigned popped ].index_by(&:itself)
end
-24
View File
@@ -1,24 +0,0 @@
module Bucket::View::Summarized
def summary
[ order_by_summary, status_summary, tag_summary, assignee_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 <mark>#{assignees.pluck(:name).to_choice_sentence}</mark>" if assignees.any?
end
def tag_summary
"tagged <mark>#{tags.map(&:hashtag).to_choice_sentence}</mark>" if tags.any?
end
end
-12
View File
@@ -1,12 +0,0 @@
module Bucket::View::Tags
extend ActiveSupport::Concern
included do
store_accessor :filters, :tag_ids
end
private
def tags
@tags ||= account.tags.where id: tag_ids
end
end
-7
View File
@@ -1,7 +0,0 @@
module Bucket::Views
extend ActiveSupport::Concern
included do
has_many :views, dependent: :delete_all
end
end
+53
View File
@@ -0,0 +1,53 @@
class Filter < ApplicationRecord
include Assignments, Buckets, Indexes, Summarized, Tags
KNOWN_PARAMS = %i[ indexed_by bucket_ids assignments tag_ids ]
belongs_to :creator, class_name: "User", default: -> { Current.user }
has_one :account, through: :creator
class << self
def default_params
{ "indexed_by" => "most_active" }
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
end
end
def to_params
ActionController::Parameters.new(params).permit(*KNOWN_PARAMS).tap do |params|
params[:filter_id] = id if persisted?
end
end
def savable?
!bucket_default?
end
def cacheable?
buckets.exists?
end
def cache_key
ActiveSupport::Cache.expand_cache_key buckets.cache_key_with_version, super
end
private
def bucket_default?
non_default_params.keys == %w[ bucket_ids ] && buckets.one?
end
def non_default_params
params.reject { |k, v| self.class.default_params[k] == v }
end
end
+9
View File
@@ -0,0 +1,9 @@
module Filter::Assignments
def assignments
params["assignments"].to_s.inquiry
end
def assignees
@assignees ||= account.users.where id: assignments.split(",")
end
end
+11
View File
@@ -0,0 +1,11 @@
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
+7
View File
@@ -0,0 +1,7 @@
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
+36
View File
@@ -0,0 +1,36 @@
module Filter::Summarized
def summary
[ index_summary, tag_summary, assignee_summary ].compact.to_sentence + " #{bucket_summary}"
end
def plain_summary
summary.remove(/<\/?mark>/)
end
private
def index_summary
"<mark>#{indexed_by.humanize}</mark>"
end
def tag_summary
if tags.exists?
"tagged <mark>#{tags.map(&:hashtag).to_choice_sentence}</mark>"
end
end
def assignee_summary
if assignees.exists?
"assigned to <mark>#{assignees.pluck(:name).to_choice_sentence}</mark>"
elsif assignments.unassigned?
"assigned to no one"
end
end
def bucket_summary
if buckets.exists?
"in <mark>#{buckets.pluck(:name).to_choice_sentence}</mark>"
else
"in <mark>all projects</mark>"
end
end
end
+11
View File
@@ -0,0 +1,11 @@
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 -1
View File
@@ -1,4 +1,4 @@
class Pop < ApplicationRecord
belongs_to :bubble
belongs_to :bubble, touch: true
belongs_to :user, optional: true
end
+4 -3
View File
@@ -1,6 +1,4 @@
class User < ApplicationRecord
include Viewer
belongs_to :account
has_many :sessions, dependent: :destroy
@@ -8,7 +6,10 @@ class User < ApplicationRecord
has_many :accesses, dependent: :destroy
has_many :buckets, through: :accesses
has_many :bubbles, through: :buckets
has_many :accessible_bubbles, through: :buckets, source: :bubbles
has_many :filters, foreign_key: :creator_id, inverse_of: :creator, dependent: :destroy
has_many :pops, dependent: :nullify
has_many :assignments, foreign_key: :assignee_id, dependent: :destroy
-7
View File
@@ -1,7 +0,0 @@
module User::Viewer
extend ActiveSupport::Concern
included do
has_many :bucket_views, class_name: "Bucket::View", foreign_key: :creator_id, dependent: :destroy
end
end
+7 -19
View File
@@ -1,28 +1,16 @@
<div class="flex flex-column gap-half align-center">
<h1 class="txt-large flex align-center gap-half" style="margin-inline-end: calc(var(--btn-size) / -2);">
<%= render "bubbles/filters/main", bucket: bucket %>
<%= bubble_filters_heading filter do %>
<%= render "bubbles/filters/index", filter: filter %>
in
<%= render "bubbles/filters/bucket", bucket: bucket %>
<%= render "bubbles/filters/buckets", filter: filter %>
<% if false %>
<%= button_tag type: :submit, class: "btn txt-small borderless" do %>
<%= image_tag "bookmark.svg", aria: { hidden: true }, size: 24 %>
<span class="for-screen-reader">Save this filter</span>
<% end %>
<% else %>
<%= 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>
<% end %>
<%= render "bubbles/filters/bookmark", filter: filter if filter.savable? %>
<% end %>
</h1>
<h2 class="txt-medium flex align-center justify-center gap-half">
<%= filter.tags ? "Tagged" : "with" %>
<%= render "bubbles/filters/tags", bucket: bucket, filter: filter %>
<%= filter.tags.present? ? "Tagged" : "with" %>
<%= render "bubbles/filters/tags", filter: filter %>
<% unless unassigned_filter_activated? %>
and <%= render "bubbles/filters/assignees", bucket: bucket, filter: filter %>
<% end %>
and <%= render "bubbles/filters/assignments", filter: filter %>
</h2>
</div>
+1 -1
View File
@@ -1,5 +1,5 @@
<% bubble.tags.each do |tag| %>
<%= link_to "##{tag.title}", bucket_bubbles_path(bubble.bucket, tag_id: tag), class: "bubble__bubble bubble__meta bubble__tag" %>
<%= link_to tag.hashtag, bubbles_path(bucket_id: bubble.bucket, tag_ids: tag), class: "bubble__bubble bubble__meta bubble__tag" %>
<% end %>
<% if bubble.tags.size < 3 %>
@@ -1,18 +1,20 @@
<div class="filter align-center gap-half" data-controller="dialog" data-action="keydown.esc->dialog#close click@document->dialog#closeOnClickOutside">
<button class="btn btn--plain filter__button" data-action="click->dialog#toggle" data-dialog-modal-value="true">
<%= assignee_filter_text filter %>
<%= assignments_filter_text filter %>
</button>
<dialog class="filter__popup panel fill-white shadow" data-dialog-target="dialog">
<dialog class="filter__popup panel fill-white shadow" data-dialog-target="dialog" data-turbo-temporary>
<menu class="filter__menu unpad margin-none">
<% bucket.users.active.order(:name).each do |user| %>
<li><%= link_to user.name, bucket_bubbles_path(bucket, view_filter_params.merge(assignee_ids: [ user.id ])) %></li>
<li><%= link_to "No one", bubbles_path(filter.to_params.merge(assignments: :unassigned)), 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>
<% end %>
</menu>
</dialog>
<% if filter.assignees %>
<%= link_to bucket_bubbles_path(bucket, view_filter_params.without(:assignee_ids)), class: "btn", style: "font-size: 0.4em;" do %>
<% if filter.assignments.present? %>
<%= link_to bubbles_path(filter.to_params.without(:assignments)), 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 %>
@@ -0,0 +1,16 @@
<% if params[:filter_id] %>
<%= button_to filter_path(params[:filter_id]), method: :delete, class: "btn txt-small borderless" do %>
<%= image_tag "bookmark.svg", aria: { hidden: true }, size: 24 %>
<span class="for-screen-reader">Delete this filter</span>
<% 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? %>
<% 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>
<% end %>
<% end %>
<% end %>
@@ -1,19 +0,0 @@
<% view ||= Bucket::View.new %>
<%= bubble_filter_form_tag bucket_views_path(bucket), method: :post, id: dom_id(view, :new_form) %>
<% if view.persisted? %>
<%= bubble_filter_form_tag bucket_view_path(bucket, view), method: :put, id: dom_id(view, :edit_form) %>
<% end %>
<%= button_tag type: :submit, form: dom_id(view, :new_form), class: "btn", style: "font-size: 0.4em;" do %>
<%= image_tag "add.svg", aria: { hidden: true }, size: 24 %>
<span class="txt-small">New</span>
<% end %>
<% if view.persisted? %>
<%= button_tag type: :submit, form: dom_id(view, :edit_form), class: "btn", style: "font-size: 0.4em;" do %>
<%= image_tag "pencil.svg", aria: { hidden: true }, size: 24 %>
<span class="txt-small">Update</span>
<% end %>
<% end %>
@@ -0,0 +1,15 @@
<div class="filter align-center gap-half" data-controller="dialog" data-action="keydown.esc->dialog#close click@document->dialog#closeOnClickOutside">
<button class="btn btn--plain filter__button" data-action="click->dialog#toggle" data-dialog-modal-value="true">
<%= buckets_filter_text filter %>
</button>
<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 "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>
<% end %>
</menu>
</dialog>
</div>
@@ -1,12 +1,12 @@
<div class="filter align-center gap-half" data-controller="dialog" data-action="keydown.esc->dialog#close click@document->dialog#closeOnClickOutside">
<button class="btn btn--plain filter__button" data-action="click->dialog#toggle" data-dialog-modal-value="true">
<%= bucket.name %>
<%= filter.indexed_by.humanize %>
</button>
<dialog class="filter__popup panel fill-white shadow" data-dialog-target="dialog">
<dialog class="filter__popup panel fill-white shadow" data-dialog-target="dialog" data-turbo-temporary>
<menu class="filter__menu unpad margin-none">
<% Current.user.buckets.order(:name).each do |bucket| %>
<li><%= link_to bucket.name, bucket_bubbles_path(bucket, view_filter_params), class: "filter__button" %></li>
<% Filter::INDEXES.each do |index| %>
<li><%= link_to index.humanize, bubbles_path(filter.to_params.merge(indexed_by: index)), class: "filter__button" %></li>
<% end %>
</menu>
</dialog>
-17
View File
@@ -1,17 +0,0 @@
<div class="filter align-center gap-half" data-controller="dialog" data-action="keydown.esc->dialog#close click@document->dialog#closeOnClickOutside">
<button class="btn btn--plain filter__button" data-action="click->dialog#toggle" data-dialog-modal-value="true">
<%= main_filter_text %>
</button>
<dialog class="filter__popup panel fill-white shadow" data-dialog-target="dialog">
<menu class="filter__menu unpad margin-none">
<% Bucket::View::ORDERS.each do |key, value| %>
<li><%= link_to value.humanize, bucket_bubbles_path(bucket, view_filter_params.merge(order_by: key, status: nil)), class: "filter__button" %></li>
<% end %>
<% Bucket::View::STATUSES.each do |key, value| %>
<li><%= link_to value.humanize, bucket_bubbles_path(bucket, view_filter_params.merge(order_by: nil, status: key)), class: "filter__button" %></li>
<% end %>
</menu>
</dialog>
</div>
+6 -6
View File
@@ -1,18 +1,18 @@
<div class="filter align-center gap-half" data-controller="dialog" data-action="keydown.esc->dialog#close click@document->dialog#closeOnClickOutside">
<button class="btn btn--plain filter__button" data-action="click->dialog#toggle" data-dialog-modal-value="true">
<%= tag_filter_text filter %>
<%= tags_filter_text filter %>
</button>
<dialog class="filter__popup panel fill-white shadow" data-dialog-target="dialog">
<dialog class="filter__popup panel fill-white shadow" data-dialog-target="dialog" data-turbo-temporary>
<menu class="filter__menu unpad margin-none">
<% bucket.tags.order(:title).each do |tag| %>
<li><%= link_to tag.title, bucket_bubbles_path(bucket, view_filter_params.merge(tag_ids: [ tag.id ])) %></li>
<% Current.account.tags.order(:title).each do |tag| %>
<li><%= link_to tag.title, bubbles_path(filter.to_params.merge(tag_ids: tag.id)) %></li>
<% end %>
</menu>
</dialog>
<% if filter.tags %>
<%= link_to bucket_bubbles_path(bucket, view_filter_params.without(:tag_ids)), class: "btn", style: "font-size: 0.4em;" do %>
<% if filter.tags.present? %>
<%= link_to bubbles_path(filter.to_params.without(:tag_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 %>
+16 -10
View File
@@ -1,24 +1,30 @@
<% @page_title = @bucket.name %>
<% @page_title = @filter.plain_summary %>
<% content_for :header do %>
<nav>
<%= link_to buckets_path, class: "btn btn--plain flex-item-justify-start" do %>
<%= link_to root_path, class: "btn btn--plain flex-item-justify-start" do %>
<%= image_tag "bubbles.svg", aria: { hidden: true }, size: 24 %>
<span class="for-screen-reader">All Buckets</span>
<span class="for-screen-reader">All Projects</span>
<% end %>
<header class="txt-align-center">
<%= render "bubbles/filters", bucket: @bucket, view: @view, filter: @filter %>
<%= render "bubbles/filters", filter: @filter %>
</header>
<%= button_to bucket_bubbles_path(@bucket), method: :post, class: "btn btn--plain", form_class: "flex-item-justify-end" do %>
<%= image_tag "bubble-add.svg", aria: { hidden: true }, size: 24 %>
<span class="for-screen-reader">Create a new bubble</span>
<% if @filter.buckets.any? %>
<%= button_to bucket_bubbles_path(@filter.buckets.first), method: :post, class: "btn btn--plain", form_class: "flex-item-justify-end" do %>
<%= image_tag "bubble-add.svg", aria: { hidden: true }, size: 24 %>
<span class="for-screen-reader">Create a new bubble</span>
<% end %>
<% else %>
<span class="flex-item-justify-end" style="height: var(--btn-size); width: var(--btn-size);">
<%# FIXME: This is a placeholder to keep the same layout without a "new bubble" button %>
</span>
<% end %>
</nav>
<% end %>
<section class="windshield flex-inline flex-wrap gap justify-center align-end" style="view-transition-name: windshield_<%= @bucket.id %>">
<section class="windshield flex-inline flex-wrap gap justify-center align-end" style="view-transition-name: windshield_<%= params[:filter_id] %>">
<% if @bubbles.any? %>
<%= render partial: "bubbles/bubble", collection: @bubbles.limit(10), cached: true %>
<% else %>
@@ -28,12 +34,12 @@
<section class="bubbles-list unpad-inline center margin-block-start">
<header class="flex-inline align-center gap txt-small margin-block-end-half center">
<%= bubble_filter_form_tag bucket_bubbles_path(@bucket), method: :get do %>
<%= form_tag bubbles_path(@filter.to_params), method: :get do %>
<%= search_field_tag :term, params[:term], class: "input center flex-inline", placeholder: "Type to filter…" %>
<% end %>
</header>
<ul class="unpad margin-none flex flex-column txt-align-start center">
<%= render partial: "bubbles/list/bubble", collection: @bubbles %>
<%= render partial: "bubbles/list/bubble", collection: @bubbles, cached: true %>
</ul>
</section>
+4 -2
View File
@@ -20,8 +20,10 @@
<small class="flex align-center gap flex-item-no-shrink">
<% bubble.tags.each do |tag| %>
<%# FIXME: This is not going to work, this partial must be cacheable, so it can't mutate a changeable view filter live. Need a JS solution. %>
<%= link_to "##{tag.title}", bucket_bubbles_path(bubble.bucket, view_filter_params.merge(tag_ids: tag.id)), style: "color: #{bubble.color}" %>
<%= button_tag tag.hashtag, type: :button,
class: "btn btn--plain", style: "color: #{bubble.color}", data: {
controller: "query-merger", action: "query-merger#merge",
query_merger_key_param: "tag_ids", query_merger_value_param: tag.id } %>
<% end %>
<%= tag.time bubble.created_at, class: "txt-nowrap flex-item-justify-end" do %>
+2 -2
View File
@@ -2,9 +2,9 @@
<% content_for :header do %>
<nav>
<%= link_to bucket_bubbles_path(@bubble.bucket), class: "btn flex-item-justify-start" do %>
<%= link_to "#", class: "btn flex-item-justify-start", data: { controller: "back-navigation", back_navigation_fallback_destination_value: bubbles_path(bucket_id: @bubble.bucket) } do %>
<%= image_tag "arrow-left.svg", aria: { hidden: true }, size: 24 %>
<span class="for-screen-reader">All Bubbles</span>
<span class="for-screen-reader">Back</span>
<% end %>
<section class="flex flex-column align-end gap">
+20 -21
View File
@@ -1,23 +1,22 @@
<li class="bucket flex flex-column txt-align-center max-width position-relative">
<%= link_to bucket_bubbles_path(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.active.ordered_by_activity.limit(10).each do |bubble| %>
<div class="bubble" style="--bubble-color: <%= bubble.color %>; <%= bubble_rotation(bubble) %> <%= bubble_size(bubble) %>">
<svg class="bubble__svg" style="fill: <%= bubble.color %>; stroke: <%= bubble.color %>;" viewBox="0 0 990 990" xmlns="http://www.w3.org/2000/svg">
<path d="m0 0h990v990h-990z" fill="none" stroke="none" />
<path d="m391.65 879.47c-110.52-15.95-212.21-91.86-255.92-191.23-66.78-143.65-41.62-347.61 48.08-481.17 368.33-516.3 1252.97 520.2 451.03 660.78-44.07 8.84-88.98 13.49-133.01 15.68-36.69 2-73.37 1.91-109.99-4.03z"/>
</svg>
</div>
<% 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 %>
<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) %>">
<svg class="bubble__svg" style="fill: <%= bubble.color %>; stroke: <%= bubble.color %>;" viewBox="0 0 990 990" xmlns="http://www.w3.org/2000/svg">
<path d="m0 0h990v990h-990z" fill="none" stroke="none" />
<path d="m391.65 879.47c-110.52-15.95-212.21-91.86-255.92-191.23-66.78-143.65-41.62-347.61 48.08-481.17 368.33-516.3 1252.97 520.2 451.03 660.78-44.07 8.84-88.98 13.49-133.01 15.68-36.69 2-73.37 1.91-109.99-4.03z"/>
</svg>
</div>
<% end %>
</div>
<% 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 %>
<strong class="txt-x-large">In <%= bucket.name %></strong>
<% end %>
</div>
<% end %>
<div class="flex align-center justify-center flex-column gap-half center">
<strong><%= link_to bucket.name, bucket_bubbles_path(bucket), class: "txt-x-large txt-ink" %></strong>
<%= link_to edit_bucket_url(bucket), class: "btn" do %>
<%= image_tag "settings.svg", aria: { hidden: true }, size: 24 %>
<span class="for-screen-reader">Bucket settings</span>
<% end %>
</div>
</li>
</li>
<% end %>
-29
View File
@@ -1,29 +0,0 @@
<% path = bucket_bubbles_path(view.bucket, **view.filters, view_id: view.id) %>
<li class="bucket flex flex-column txt-align-center max-width position-relative">
<%= link_to path, 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_<%= view.bucket.id %>">
<% view.bubbles.ordered_by_activity.limit(10).each do |bubble| %>
<div class="bubble" style="--bubble-color: <%= bubble.color %>; <%= bubble_rotation(bubble) %> <%= bubble_size(bubble) %>">
<svg class="bubble__svg" style="fill: <%= bubble.color %>; stroke: <%= bubble.color %>;" viewBox="0 0 990 990" xmlns="http://www.w3.org/2000/svg">
<path d="m0 0h990v990h-990z" fill="none" stroke="none" />
<path d="m391.65 879.47c-110.52-15.95-212.21-91.86-255.92-191.23-66.78-143.65-41.62-347.61 48.08-481.17 368.33-516.3 1252.97 520.2 451.03 660.78-44.07 8.84-88.98 13.49-133.01 15.68-36.69 2-73.37 1.91-109.99-4.03z"/>
</svg>
</div>
<% end %>
</div>
<% end %>
<div class="flex align-center justify-center flex-column flex-wrap center gap-half">
<%= link_to path, class: "txt-ink flex flex-column" do %>
<strong class="txt-x-large"><%= view.summary.html_safe %> in <mark><%= view.bucket.name %></mark></strong>
<% end %>
<%= button_tag type: :submit, form: dom_id(view, :delete_form), class: "btn btn--negative", data: { turbo_confirm: "Are you sure you want to delete this filter?" } do %>
<%= image_tag "minus.svg", aria: { hidden: true }, size: 24 %>
<span class="for-screen-reader">Delete</span>
<% end %>
<%= bubble_filter_form_tag bucket_view_path(view.bucket, view), method: :delete, id: dom_id(view, :delete_form) %>
</div>
</li>
+2 -2
View File
@@ -37,13 +37,13 @@
<% end %>
<div data-filter-target="list">
<%= render partial: "buckets/user_toggle", collection: @selected_users, as: :user, locals: { bucket: @bucket, selected: true } %>
<%= render partial: "buckets/user_toggle", collection: @selected_users, as: :user, locals: { selected: true } %>
<% if @selected_users.any? && @unselected_users.any? %>
<hr class="separator full-width" style="--border-style: solid">
<% end %>
<%= render partial: "buckets/user_toggle", collection: @unselected_users, as: :user, locals: { bucket: @bucket, selected: false } %>
<%= render partial: "buckets/user_toggle", collection: @unselected_users, as: :user, locals: { selected: false } %>
</div>
</menu>
</section>
+4 -9
View File
@@ -1,4 +1,4 @@
<% @page_title = "Buckets" %>
<% @page_title = "Projects" %>
<% content_for :header do %>
<nav>
@@ -9,17 +9,12 @@
<%= link_to new_bucket_path, class: "btn btn--plain flex-item-justify-end", style: "view-transition-name: new-bucket" do %>
<%= image_tag "bubbles-add.svg", aria: { hidden: true }, size: 24 %>
<span class="for-screen-reader">Add a new bucket</span>
<span class="for-screen-reader">Add a new project</span>
<% end %>
</nav>
<% end %>
<menu class="buckets margin-none unpad align-center justify-center flex flex-wrap gap">
<% @buckets.each do |bucket| %>
<% if bucket.is_a? Bucket %>
<%= render partial: "buckets/bucket", locals: { bucket: bucket } %>
<% elsif bucket.is_a? Bucket::View %>
<%= render partial: "buckets/view", locals: { view: bucket } %>
<% end %>
<% end %>
<%= render @filters %>
<%= render @buckets, cached: true %>
</menu>
+26
View File
@@ -0,0 +1,26 @@
<% cache_if filter.cacheable?, filter do %>
<li class="bucket flex flex-column txt-align-center max-width position-relative">
<%= link_to bubbles_path(**filter.to_params), 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_<%= filter.id %>">
<% filter.bubbles.ordered_by_activity.limit(10).each do |bubble| %>
<div class="bubble" style="--bubble-color: <%= bubble.color %>; <%= bubble_rotation(bubble) %> <%= bubble_size(bubble) %>">
<svg class="bubble__svg" style="fill: <%= bubble.color %>; stroke: <%= bubble.color %>;" viewBox="0 0 990 990" xmlns="http://www.w3.org/2000/svg">
<path d="m0 0h990v990h-990z" fill="none" stroke="none" />
<path d="m391.65 879.47c-110.52-15.95-212.21-91.86-255.92-191.23-66.78-143.65-41.62-347.61 48.08-481.17 368.33-516.3 1252.97 520.2 451.03 660.78-44.07 8.84-88.98 13.49-133.01 15.68-36.69 2-73.37 1.91-109.99-4.03z"/>
</svg>
</div>
<% end %>
</div>
<% end %>
<div class="flex align-center justify-center flex-column flex-wrap center gap-half">
<%= link_to bubbles_path(**filter.to_params), class: "txt-ink flex flex-column" do %>
<strong class="txt-x-large"><%= filter.summary.html_safe %></strong>
<% end %>
<%= button_to filter_path(filter), method: :delete, class: "btn btn--negative", data: { turbo_confirm: "Are you sure you want to delete this filter?" } do %>
<%= image_tag "minus.svg", aria: { hidden: true }, size: 24 %>
<span class="for-screen-reader">Delete</span>
<% end %>
</div>
</li>
<% end %>
+3 -4
View File
@@ -12,6 +12,8 @@ Rails.application.routes.draw do
route_for :bucket_bubble, bubble.bucket, bubble, options
end
resources :bubbles
resources :buckets do
resources :bubbles do
resources :assignments
@@ -28,12 +30,9 @@ Rails.application.routes.draw do
end
resources :tags, only: :index
scope module: :buckets do
resources :views
end
end
resources :filters
resource :first_run
resource :session
@@ -0,0 +1,14 @@
class RenameBucketViewsToFilters < ActiveRecord::Migration[8.0]
def change
rename_table :bucket_views, :filters
remove_index :filters, %i[ bucket_id creator_id filters ], unique: true
remove_index :filters, :creator_id
remove_column :filters, :bucket_id
rename_column :filters, :filters, :params
add_index :filters, %i[ creator_id params ], unique: true
end
end
Generated
+9 -11
View File
@@ -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_10_28_183212) do
ActiveRecord::Schema[8.0].define(version: 2024_11_05_181312) do
create_table "accesses", force: :cascade do |t|
t.integer "bucket_id", null: false
t.integer "user_id", null: false
@@ -82,16 +82,6 @@ ActiveRecord::Schema[8.0].define(version: 2024_10_28_183212) do
t.index ["stage_id"], name: "index_bubbles_on_stage_id"
end
create_table "bucket_views", force: :cascade do |t|
t.integer "creator_id", null: false
t.integer "bucket_id", null: false
t.json "filters", default: {}, null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["bucket_id", "creator_id", "filters"], name: "index_bucket_views_on_bucket_id_and_creator_id_and_filters", unique: true
t.index ["creator_id"], name: "index_bucket_views_on_creator_id"
end
create_table "buckets", force: :cascade do |t|
t.integer "account_id", null: false
t.integer "creator_id", null: false
@@ -125,6 +115,14 @@ ActiveRecord::Schema[8.0].define(version: 2024_10_28_183212) do
t.index ["summary_id", "action"], name: "index_events_on_summary_id_and_action"
end
create_table "filters", force: :cascade do |t|
t.integer "creator_id", null: false
t.json "params", default: {}, null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["creator_id", "params"], name: "index_filters_on_creator_id_and_params", unique: true
end
create_table "messages", force: :cascade do |t|
t.integer "bubble_id", null: false
t.string "messageable_type", null: false
+3
View File
@@ -0,0 +1,3 @@
jz_assignments:
creator: jz
params: <%= { indexed_by: :most_discussed, assignments: ActiveRecord::FixtureSet.identify(:jz).to_s, tag_ids: ActiveRecord::FixtureSet.identify(:mobile).to_s }.to_json %>