Spike persisted filters

This commit is contained in:
Jose Farias
2024-10-16 16:41:11 -06:00
parent c9ac10cbdb
commit f4b5210ca1
35 changed files with 354 additions and 112 deletions
+22 -17
View File
@@ -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
@@ -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
+2 -2
View File
@@ -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
-4
View File
@@ -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
+26
View File
@@ -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
+20 -7
View File
@@ -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
+2 -2
View File
@@ -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)
+4
View File
@@ -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
+2
View File
@@ -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)
+2 -2
View File
@@ -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?
+14
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -1,5 +1,5 @@
class Bucket < ApplicationRecord
include Accessible
include Accessible, Views
belongs_to :account
belongs_to :creator, class_name: "User", default: -> { Current.user }
+37
View File
@@ -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
+10
View File
@@ -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
+24
View File
@@ -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
+10
View File
@@ -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
+7
View File
@@ -0,0 +1,7 @@
module Bucket::Views
extend ActiveSupport::Concern
included do
has_many :views, dependent: :delete_all
end
end
+2
View File
@@ -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
+1 -1
View File
@@ -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
+4
View File
@@ -3,4 +3,8 @@ class Tag < ApplicationRecord
has_many :taggings, dependent: :destroy
has_many :bubbles, through: :taggings
def hashtag
"#" + title
end
end
+2
View File
@@ -1,4 +1,6 @@
class User < ApplicationRecord
include Viewer
belongs_to :account
has_many :sessions, dependent: :destroy
+7
View File
@@ -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
+44 -35
View File
@@ -1,21 +1,18 @@
<h1 class="txt-large">
<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">
<% if params[:popped] %>
Popped
<% else %>
Most active
<% end %>
<%= main_filter_text %>
</button>
<dialog class="filter__popup panel fill-white shadow" data-dialog-target="dialog">
<menu class="filter__menu unpad margin-none">
<li><%= link_to "Most active", bucket_bubbles_path(bucket, bubble_filter_params.without(:popped)), class: "filter__button" %></li>
<li><a href="#" class="filter__button">Most discussed</a></li>
<li><a href="#" class="filter__button">Most boosted</a></li>
<li><a href="#" class="filter__button">Newest</a></li>
<li><a href="#" class="filter__button">Oldest</a></li>
<li><%= link_to "Popped", bucket_bubbles_path(bucket, bubble_filter_params.merge(popped: true)), class: "filter__button" %></li>
<% Bucket::View::ORDERS.each do |key, value| %>
<li><%= link_to value.upcase_first, bucket_bubbles_path(bucket, bubble_filter_params.merge(order_by: key, status: nil)), class: "filter__button" %></li>
<% end %>
<% Bucket::View::STATUSES.each do |key, value| %>
<li><%= link_to value.upcase_first, bucket_bubbles_path(bucket, bubble_filter_params.merge(order_by: nil, status: key)), class: "filter__button" %></li>
<% end %>
</menu>
</dialog>
</div>
@@ -30,7 +27,7 @@
<dialog class="filter__popup panel fill-white shadow" data-dialog-target="dialog">
<menu class="filter__menu unpad margin-none">
<% Current.user.buckets.order(:name).each do |bucket| %>
<li><%= link_to bucket.name, bucket_bubbles_path(bucket), class: "filter__button" %></li>
<li><%= link_to bucket.name, bucket_bubbles_path(bucket, bubble_filter_params), class: "filter__button" %></li>
<% end %>
</menu>
</dialog>
@@ -43,49 +40,61 @@
</h1>
<h2 class="txt-medium flex align-center justify-center gap-half">
<%= tag ? "Tagged" : "with" %>
<%= @tag_filters ? "Tagged" : "with" %>
<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&.title || "any tag" %>
<%= tag_filter_text %>
</button>
<dialog class="filter__popup panel fill-white shadow" data-dialog-target="dialog">
<menu class="filter__menu unpad margin-none">
<% bucket.tags.order(:title).each do |tag| %>
<li><%= link_to tag.title, bucket_bubbles_path(bucket, bubble_filter_params.merge(tag_id: tag.id)) %></li>
<li><%= link_to tag.title, bucket_bubbles_path(bucket, bubble_filter_params.merge(tag_ids: [ tag.id ])) %></li>
<% end %>
</menu>
</dialog>
<% if tag %>
<%= link_to bucket_bubbles_path(bucket, bubble_filter_params.without(:tag_id)), class: "btn", style: "font-size: 0.4em;" do %>
<% if @tag_filters %>
<%= link_to bucket_bubbles_path(bucket, bubble_filter_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 %>
<% end %>
</div>
and
<% unless status_filter_param.unassigned? %>
and
<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">
assigned to <%= assignee&.name || "anyone" %>
</button>
<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 %>
</button>
<dialog class="filter__popup panel fill-white shadow" data-dialog-target="dialog">
<menu class="filter__menu unpad margin-none">
<% bucket.users.active.order(:name).each do |user| %>
<li><%= link_to user.name, bucket_bubbles_path(bucket, bubble_filter_params.merge(assignee_id: user.id)) %></li>
<dialog class="filter__popup panel fill-white shadow" data-dialog-target="dialog">
<menu class="filter__menu unpad margin-none">
<% bucket.users.active.order(:name).each do |user| %>
<li><%= link_to user.name, bucket_bubbles_path(bucket, bubble_filter_params.merge(assignee_ids: [ user.id ])) %></li>
<% end %>
</menu>
</dialog>
<% if @assignee_filters %>
<%= link_to bucket_bubbles_path(bucket, bubble_filter_params.without(: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 %>
</menu>
</dialog>
<% if assignee %>
<%= link_to bucket_bubbles_path(bucket, bubble_filter_params.without(:assignee_id)), 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 %>
<% end %>
</div>
</div>
<% end %>
<%= button_to bucket_views_path(bucket), class: "btn", style: "font-size: 0.4em;" do %>
<%= hidden_field_tag :order_by, params[:order_by], name: "view[order_by]" %>
<%= hidden_field_tag :status, params[:status], name: "view[status]" %>
<%= hidden_field_tag :assignee_ids, Array(params[:assignee_ids]).join(","), name: "view[assignee_ids]" %>
<%= hidden_field_tag :tag_ids, Array(params[:tag_ids]).join(","), name: "view[tag_ids]" %>
<%= image_tag "add.svg", aria: { hidden: true }, size: 24 %>
<span class="for-screen-reader">Save filters</span>
<% end %>
</h2>
+3 -3
View File
@@ -19,8 +19,8 @@
<% end %>
<section class="windshield flex-inline flex-wrap gap justify-center align-end" style="view-transition-name: windshield_<%= @bucket.id %>">
<% if @bubbles.ordered_by_activity.any? %>
<%= render partial: "bubbles/bubble", collection: @bubbles.ordered_by_activity.limit(10) %>
<% if @bubbles.any? %>
<%= render partial: "bubbles/bubble", collection: @bubbles.limit(10) %>
<% else %>
<p><strong>Nothing here.</strong></p>
<% end %>
@@ -37,6 +37,6 @@
</header>
<ul class="unpad margin-none flex flex-column txt-align-start center">
<%= render partial: "bubbles/list/bubble", collection: @bubbles.ordered_by_activity %>
<%= render partial: "bubbles/list/bubble", collection: @bubbles %>
</ul>
</section>
+1 -1
View File
@@ -24,7 +24,7 @@
<small class="flex align-center gap flex-item-no-shrink">
<% bubble.tags.each do |tag| %>
<%= link_to "##{tag.title}", bucket_bubbles_path(bubble.bucket, tag_id: tag.id), style: "color: #{bubble.color}" %>
<%= link_to "##{tag.title}", bucket_bubbles_path(bubble.bucket, tag_ids: [ tag.id ]), style: "color: #{bubble.color}" %>
<% end %>
<%= tag.time bubble.created_at, class: "txt-nowrap flex-item-justify-end" do %>
+18
View File
@@ -0,0 +1,18 @@
<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.not_popped.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 gap-half center">
<strong><%= link_to bucket.name, bucket_bubbles_path(bucket), class: "txt-x-large txt-ink" %></strong>
</div>
</li>
+21
View File
@@ -0,0 +1,21 @@
<li class="bucket flex flex-column txt-align-center max-width position-relative">
<%= link_to bucket_bubbles_path(view.bucket, **view.to_bucket_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_<%= view.bucket.id %>">
<% view.bucket.bubbles.not_popped.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 center">
<%= link_to bucket_bubbles_path(view.bucket, **view.to_bucket_params), class: "txt-ink" do %>
<strong class="txt-x-large"><%= view.bucket.name %></strong>
<p class="margin-none"><%= view.summary %></p>
<% end %>
</div>
</li>
+5 -23
View File
@@ -16,28 +16,10 @@
<menu class="buckets margin-none unpad align-center justify-center flex flex-wrap gap">
<% @buckets.each do |bucket| %>
<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.not_popped.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 gap-half center">
<strong><%= link_to bucket.name, bucket_bubbles_path(bucket), class: "txt-x-large txt-ink" %></strong>
<%= link_to edit_bucket_path(bucket), class: "btn txt-small", hidden: true do %>
<%= image_tag "pencil.svg", aria: { hidden: true }, size: 24 %>
<span class="for-screen-reader">Edit bucket</span>
<% end %>
</div>
</li>
<% 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 %>
</menu>
+1 -1
View File
@@ -6,7 +6,7 @@
<strong><%= Current.user.name %></strong>
</div>
<div class="comment__body txt-align-start margin-block-start-half">
<%= form_with model: Comment.new, url: bucket_bubble_comments_path(bubble.bucket, bubble), class: "flex flex-column gap full-width" do |form| %>
<%= form_with model: Comment.new, url: bucket_bubble_comments_path(bubble.bucket, bubble), class: "flex flex-column gap full-width", data: { controller: "form", action: "keydown.meta+enter->form#submit" } do |form| %>
<%= form.text_area :body, class: "input", required: true, rows: 4,
placeholder: (bubble.comments.empty? && bubble.creator == Current.user) ? "Add some notes…" : "Type your comment…" %>
+3
View File
@@ -0,0 +1,3 @@
%w[ rails_ext ].each do |extensions_dir|
Dir["#{Rails.root}/lib/#{extensions_dir}/*"].each { |path| require "#{extensions_dir}/#{File.basename(path)}" }
end
+10 -11
View File
@@ -2,31 +2,30 @@ Rails.application.routes.draw do
root "buckets#index"
resource :first_run
resource :session
resource :account do
scope module: "accounts" do
scope module: :accounts do
resource :join_code
resources :users
end
end
get "join/:join_code", to: "users#new", as: :join
post "join/:join_code", to: "users#create"
resources :users do
scope module: "users" do
scope module: :users do
resource :avatar
end
end
resources :buckets do
resource :access, controller: "buckets/accesses"
resources :tags, only: :index
scope module: :buckets do
resources :views
end
resources :bubbles do
scope module: "bubbles" do
scope module: :bubbles do
resource :image
resource :pop
end
@@ -36,9 +35,9 @@ Rails.application.routes.draw do
resources :comments
resources :tags, shallow: true
end
resources :tags, only: :index
end
get "join/:join_code", to: "users#new", as: :join
post "join/:join_code", to: "users#create"
get "up", to: "rails/health#show", as: :rails_health_check
end
+11
View File
@@ -0,0 +1,11 @@
class CreateViews < ActiveRecord::Migration[8.0]
def change
create_table :bucket_views do |t|
t.references :creator, null: false
t.references :bucket, null: false
t.json :filters, default: {}, null: false
t.timestamps
end
end
end
Generated
+11 -1
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_09_211414) do
ActiveRecord::Schema[8.0].define(version: 2024_10_15_224007) do
create_table "accesses", force: :cascade do |t|
t.integer "bucket_id", null: false
t.integer "user_id", null: false
@@ -80,6 +80,16 @@ ActiveRecord::Schema[8.0].define(version: 2024_10_09_211414) do
t.index ["bucket_id"], name: "index_bubbles_on_bucket_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"], name: "index_bucket_views_on_bucket_id"
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
@@ -0,0 +1,5 @@
class Array
def to_choice_sentence
to_sentence two_words_connector: " or ", last_word_connector: ", or "
end
end