Spike events system
This commit is contained in:
@@ -2,6 +2,6 @@ class BoostsController < ApplicationController
|
||||
include BubbleScoped, BucketScoped
|
||||
|
||||
def create
|
||||
@bubble.boosts.create!
|
||||
@bubble.boost!
|
||||
end
|
||||
end
|
||||
|
||||
@@ -2,12 +2,7 @@ class CommentsController < ApplicationController
|
||||
include BubbleScoped, BucketScoped
|
||||
|
||||
def create
|
||||
@bubble.comments.create!(comment_params)
|
||||
@bubble.comment! params.dig(:comment, :body).presence
|
||||
redirect_to bucket_bubble_url(@bucket, @bubble)
|
||||
end
|
||||
|
||||
private
|
||||
def comment_params
|
||||
params.require(:comment).permit(:body)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -8,7 +8,7 @@ module BubblesHelper
|
||||
end
|
||||
|
||||
def bubble_size(bubble)
|
||||
activity = bubble.boosts.size + bubble.comments.size
|
||||
activity = bubble.boost_count + bubble.comments.count
|
||||
rank =
|
||||
case activity
|
||||
when 0..5 then "one"
|
||||
|
||||
@@ -1,80 +0,0 @@
|
||||
module CommentsHelper
|
||||
def render_comments_and_boosts(bubble)
|
||||
combined_collection = combine_and_sort_items(bubble)
|
||||
|
||||
safe_join([
|
||||
render_creator_summary(bubble, combined_collection),
|
||||
render_remaining_items(combined_collection)
|
||||
])
|
||||
end
|
||||
|
||||
private
|
||||
def combine_and_sort_items(bubble)
|
||||
(bubble.comments + bubble.boosts + bubble.assignments).sort_by(&:created_at)
|
||||
end
|
||||
|
||||
def render_creator_summary(bubble, combined_collection)
|
||||
content_tag(:div, class: "comment--upvotes flex-inline flex-wrap align-start gap fill-white border-radius center position-relative") do
|
||||
[
|
||||
creator_info(bubble),
|
||||
initial_assignment_info(combined_collection),
|
||||
render_initial_boosts(combined_collection)
|
||||
].compact.join(", ").html_safe
|
||||
end
|
||||
end
|
||||
|
||||
def creator_info(bubble)
|
||||
"Added by #{bubble.creator.name} #{time_ago_in_words(bubble.created_at)} ago"
|
||||
end
|
||||
|
||||
def initial_assignment_info(combined_collection)
|
||||
initial_assignment = combined_collection.find { |item| item.is_a?(Assignment) }
|
||||
"assigned to #{initial_assignment.assignee.name}" if initial_assignment
|
||||
end
|
||||
|
||||
def render_initial_boosts(combined_collection)
|
||||
grouped_boosts = combined_collection.take_while { |item| item.is_a?(Boost) }
|
||||
return if grouped_boosts.empty?
|
||||
|
||||
user_boosts = grouped_boosts.group_by(&:creator).transform_values(&:count)
|
||||
boost_summaries = user_boosts.map { |user, count| "#{user.name} +#{count}" }
|
||||
boost_summaries.to_sentence
|
||||
end
|
||||
|
||||
def render_remaining_items(combined_collection)
|
||||
initial_count = combined_collection.take_while { |item| !item.is_a?(Comment) }.count
|
||||
items = combined_collection.drop(initial_count)
|
||||
|
||||
safe_join(items.chunk_while { |i, j| grouped_item?(i) && grouped_item?(j) }.map do |chunk|
|
||||
if chunk.first.is_a?(Comment)
|
||||
render "comments/comment", comment: chunk.first
|
||||
else
|
||||
render_grouped_items(chunk)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
def grouped_item?(item)
|
||||
item.is_a?(Boost) || item.is_a?(Assignment)
|
||||
end
|
||||
|
||||
def render_grouped_items(items)
|
||||
return if items.empty?
|
||||
|
||||
content_tag(:div, class: "comment--upvotes flex-inline flex-wrap align-start gap fill-white border-radius center position-relative") do
|
||||
[
|
||||
render_grouped_boosts(items.select { |item| item.is_a?(Boost) }),
|
||||
render_grouped_assignments(items.select { |item| item.is_a?(Assignment) })
|
||||
].flatten.compact.to_sentence.html_safe
|
||||
end
|
||||
end
|
||||
|
||||
def render_grouped_boosts(boosts)
|
||||
return if boosts.empty?
|
||||
boosts.group_by(&:creator).map { |user, user_boosts| "#{user.name} +#{user_boosts.count}" }
|
||||
end
|
||||
|
||||
def render_grouped_assignments(assignments)
|
||||
assignments.map { |assignment| "Assigned to #{assignment.assignee.name} #{time_ago_in_words(assignment.created_at)} ago" }
|
||||
end
|
||||
end
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Controller } from "@hotwired/stimulus"
|
||||
import { nextFrame } from "helpers/timing_helpers"
|
||||
import { nextFrame } from "helpers"
|
||||
|
||||
export default class extends Controller {
|
||||
static classes = [ "play" ]
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Controller } from "@hotwired/stimulus"
|
||||
import { debounce } from "helpers/timing_helpers"
|
||||
import { debounce } from "helpers"
|
||||
|
||||
export default class extends Controller {
|
||||
static targets = [ "list" ]
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import { Controller } from "@hotwired/stimulus"
|
||||
import { current } from "helpers"
|
||||
|
||||
export default class extends Controller {
|
||||
static classes = [ "myComment" ]
|
||||
|
||||
connect() {
|
||||
this.#myComments.forEach(comment => comment.classList.add(this.myCommentClass))
|
||||
}
|
||||
|
||||
get #myComments() {
|
||||
return this.element.querySelectorAll(`.comment[data-creator-id='${current.user.id}']`)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
// On-demand JavaScript objects from "current" HTML <meta> elements. Example:
|
||||
//
|
||||
// <meta name="current-identity-id" content="123">
|
||||
// <meta name="current-identity-time-zone-name" content="Central Time (US & Canada)">
|
||||
//
|
||||
// >> current.identity
|
||||
// => { id: "123", timeZoneName: "Central Time (US & Canada)" }
|
||||
//
|
||||
// >> current.foo
|
||||
// => {}
|
||||
export const current = new Proxy({}, {
|
||||
get(_target, propertyName) {
|
||||
const result = {}
|
||||
const prefix = `current-${propertyName}-`
|
||||
for (const { name, content } of document.head.querySelectorAll(`meta[name^=${prefix}]`)) {
|
||||
const key = camelize(name.slice(prefix.length))
|
||||
result[key] = content
|
||||
}
|
||||
return result
|
||||
}
|
||||
})
|
||||
|
||||
function camelize(string) {
|
||||
return string.replace(/(?:[_-])([a-z0-9])/g, (_, char) => char.toUpperCase())
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from "helpers/current_helpers"
|
||||
export * from "helpers/timing_helpers"
|
||||
@@ -1,4 +0,0 @@
|
||||
class Boost < ApplicationRecord
|
||||
belongs_to :bubble
|
||||
belongs_to :creator, class_name: "User", default: -> { Current.user }
|
||||
end
|
||||
@@ -1,12 +1,9 @@
|
||||
class Bubble < ApplicationRecord
|
||||
include Assignable, Colored, Poppable, Searchable, Taggable
|
||||
include Assignable, Boostable, Colored, Commentable, Eventable, Poppable, Searchable, Taggable, Threaded
|
||||
|
||||
belongs_to :bucket
|
||||
belongs_to :creator, class_name: "User", default: -> { Current.user }
|
||||
|
||||
has_many :comments, dependent: :destroy
|
||||
has_many :boosts, dependent: :destroy
|
||||
|
||||
has_one_attached :image, dependent: :purge_later
|
||||
|
||||
searchable_by :title, using: :bubbles_search_index
|
||||
@@ -14,7 +11,7 @@ class Bubble < ApplicationRecord
|
||||
before_save :set_default_title
|
||||
|
||||
scope :reverse_chronologically, -> { order(created_at: :desc, id: :desc) }
|
||||
scope :ordered_by_activity, -> { left_joins(:comments, :boosts).group(:id).order(Arel.sql("COUNT(comments.id) + COUNT(boosts.id) DESC")) }
|
||||
scope :ordered_by_activity, -> { left_joins(:comments).group(:id).order(Arel.sql("COUNT(comments.id) + boost_count DESC")) }
|
||||
|
||||
scope :mentioning, ->(query) do
|
||||
bubbles = search(query).select(:id).to_sql
|
||||
|
||||
@@ -2,7 +2,7 @@ module Bubble::Assignable
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
has_many :assignments, dependent: :destroy
|
||||
has_many :assignments, dependent: :delete_all
|
||||
|
||||
has_many :assignees, through: :assignments
|
||||
has_many :assigners, through: :assignments
|
||||
@@ -12,8 +12,9 @@ module Bubble::Assignable
|
||||
end
|
||||
|
||||
def assign(users, assigner: Current.user)
|
||||
(Array(users) - assignees).tap do |users|
|
||||
users.each { |user| assignments.create!(assignee: user, assigner: assigner) }
|
||||
transaction do
|
||||
Assignment.insert_all Array(users).collect { |user| { assignee_id: user.id, assigner_id: assigner.id, bubble_id: id } }
|
||||
track_event :assigned, assignee_ids: Array(users).map(&:id)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
module Bubble::Boostable
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
def boost!
|
||||
transaction do
|
||||
increment! :boost_count
|
||||
track_event :boosted, boost_count:
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,11 @@
|
||||
module Bubble::Commentable
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
has_many :comments, dependent: :delete_all
|
||||
end
|
||||
|
||||
def comment!(body)
|
||||
comments.create! body:
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,14 @@
|
||||
module Bubble::Eventable
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
has_many :events, dependent: :delete_all
|
||||
|
||||
after_create -> { track_event :created }
|
||||
end
|
||||
|
||||
private
|
||||
def track_event(action, creator: Current.user, **particulars)
|
||||
events.create! action:, creator:, particulars:
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,39 @@
|
||||
class Bubble::Thread
|
||||
attr_reader :bubble
|
||||
|
||||
def initialize(bubble)
|
||||
@bubble = bubble
|
||||
end
|
||||
|
||||
def entries
|
||||
sorted_entries.chunk_while { |a, b| consecutive_events?(a, b) }.map.with_index { |entries, index| roll_up(entries, index) }
|
||||
end
|
||||
|
||||
def to_partial_path
|
||||
"bubbles/threads/thread"
|
||||
end
|
||||
|
||||
def cache_key
|
||||
ActiveSupport::Cache.expand_cache_key bubble, :thread
|
||||
end
|
||||
|
||||
private
|
||||
delegate :events, :comments, to: :bubble, private: true
|
||||
|
||||
def sorted_entries
|
||||
(events + comments).sort_by(&:created_at)
|
||||
end
|
||||
|
||||
def consecutive_events?(a, b)
|
||||
[ a, b ] in [ Event, Event ]
|
||||
end
|
||||
|
||||
def roll_up(entries, index)
|
||||
case entries.first
|
||||
when Comment
|
||||
entries.sole
|
||||
when Event
|
||||
Rollup.new self, entries, first_position: index.zero?
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,59 @@
|
||||
class Bubble::Thread::Rollup
|
||||
def initialize(thread, entries, first_position: false)
|
||||
@thread = thread
|
||||
@entries = entries
|
||||
@first_position = first_position
|
||||
end
|
||||
|
||||
def body
|
||||
collapsed_entries.map { |entry, chunk_size| summarize(entry, chunk_size) }.to_sentence.upcase_first
|
||||
end
|
||||
|
||||
def to_partial_path
|
||||
"bubbles/threads/rollup"
|
||||
end
|
||||
|
||||
def cache_key
|
||||
ActiveSupport::Cache.expand_cache_key [ thread, entries.first, entries.last ], "rollup"
|
||||
end
|
||||
|
||||
private
|
||||
attr_reader :thread, :entries, :first_position
|
||||
|
||||
delegate :time_ago_in_words, to: "ApplicationController.helpers", private: true
|
||||
|
||||
def first_position?
|
||||
first_position
|
||||
end
|
||||
|
||||
def collapsed_entries
|
||||
sorted_entries.chunk_while { |a, b| equivalent_boosts?(a, b) }.map { |chunk| [ chunk.last, chunk.size ] }
|
||||
end
|
||||
|
||||
def sorted_entries
|
||||
entries.sort_by do |entry|
|
||||
case entry.action
|
||||
when "created" then [ 1, entry.created_at ]
|
||||
when "assigned" then [ 2, entry.created_at ]
|
||||
when "boosted" then [ 3, entry.creator, entry.created_at ]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def equivalent_boosts?(a, b)
|
||||
a.action == "boosted" && a.slice(:action, :creator_id) == b.slice(:action, :creator_id)
|
||||
end
|
||||
|
||||
def summarize(entry, chunk_size)
|
||||
case entry.action
|
||||
when "created"
|
||||
"added by #{entry.creator.name} #{time_ago_in_words(entry.created_at)} ago"
|
||||
when "assigned"
|
||||
summary = "assigned to #{entry.assignee_names.to_sentence}"
|
||||
summary += " #{time_ago_in_words(entry.created_at)} ago" unless first_position?
|
||||
summary
|
||||
when "boosted"
|
||||
"#{entry.creator.name} +#{chunk_size}"
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,5 @@
|
||||
module Bubble::Threaded
|
||||
def thread
|
||||
Bubble::Thread.new self
|
||||
end
|
||||
end
|
||||
@@ -1,6 +1,10 @@
|
||||
class Current < ActiveSupport::CurrentAttributes
|
||||
attribute :session
|
||||
attribute :session, :user
|
||||
|
||||
delegate :user, to: :session, allow_nil: true
|
||||
delegate :account, to: :user, allow_nil: true
|
||||
|
||||
def session=(session)
|
||||
super
|
||||
self.user = session.user
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
class Event < ApplicationRecord
|
||||
THREADABLE_ACTIONS = %w[ assigned boosted created ]
|
||||
|
||||
include Assignments
|
||||
|
||||
belongs_to :creator, class_name: "User"
|
||||
belongs_to :bubble
|
||||
|
||||
scope :threadable, -> { where action: THREADABLE_ACTIONS }
|
||||
end
|
||||
@@ -0,0 +1,16 @@
|
||||
module Event::Assignments
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
store_accessor :particulars, :assignee_ids
|
||||
end
|
||||
|
||||
def assignee_names
|
||||
assignees.map &:name
|
||||
end
|
||||
|
||||
private
|
||||
def assignees
|
||||
@assignees ||= creator.account.users.find assignee_ids
|
||||
end
|
||||
end
|
||||
@@ -1,6 +1,6 @@
|
||||
<%= button_to bucket_bubble_boosts_path(bubble.bucket, bubble),
|
||||
class: "btn btn--plain",
|
||||
data: { action: "toggle-class#toggle" },
|
||||
form_class: "full-width" do %>
|
||||
+ <%= bubble.boosts.size if bubble.boosts.any? %>
|
||||
class: "btn btn--plain",
|
||||
data: { action: "toggle-class#toggle" },
|
||||
form_class: "full-width" do %>
|
||||
+ <%= bubble.boost_count if bubble.boost_count.positive? %>
|
||||
<% end %>
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
<%= turbo_stream.update dom_id(@bubble, :boosts) do %>
|
||||
<%= render "boosts/boosts", bubble: @bubble %>
|
||||
<% end %>
|
||||
|
||||
<%= turbo_stream.replace dom_id(@bubble, :thread) do %>
|
||||
<%= render @bubble.thread %>
|
||||
<% end %>
|
||||
|
||||
@@ -25,7 +25,4 @@
|
||||
<%= render "bubbles/bubble", bubble: @bubble %>
|
||||
</div>
|
||||
|
||||
<section class="comments align-center center borderless margin flex flex-column gap-half" style="--bubble-color: <%= @bubble.color %>;">
|
||||
<%= render_comments_and_boosts(@bubble) %>
|
||||
<%= render "comments/new", bubble: @bubble %>
|
||||
</section>
|
||||
<%= render @bubble.thread %>
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
<%# Template Dependency: comments/comment %>
|
||||
<%# Template Dependency: bubbles/threads/rollup %>
|
||||
<%= render entry %>
|
||||
@@ -0,0 +1,3 @@
|
||||
<div class="comment--upvotes flex-inline flex-wrap align-start gap fill-white border-radius center position-relative">
|
||||
<%= rollup.body %>
|
||||
</div>
|
||||
@@ -0,0 +1,13 @@
|
||||
<% bubble = thread.bubble %>
|
||||
|
||||
<section id="<%= dom_id(bubble, :thread) %>"
|
||||
class="comments align-center center borderless margin flex flex-column gap-half"
|
||||
style="--bubble-color: <%= bubble.color %>;"
|
||||
data-controller="thread"
|
||||
data-thread-my-comment-class="comment--mine">
|
||||
<% cache thread do %>
|
||||
<%= render partial: "bubbles/threads/entry", collection: thread.entries, cache: true %>
|
||||
<% end %>
|
||||
|
||||
<%= render "comments/new", bubble: bubble %>
|
||||
</section>
|
||||
@@ -20,7 +20,7 @@
|
||||
<%= 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.left_joins(:comments, :boosts).group(:id).order(Arel.sql("COUNT(comments.id) + COUNT(boosts.id) DESC")).limit(10).each do |bubble| %>
|
||||
<% 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" />
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<div class="comment flex align-start full-width <%= "comment--mine" if comment.creator == Current.user %>" id="<%= dom_id(comment) %>">
|
||||
<div id="<%= dom_id(comment) %>" class="comment flex align-start full-width" data-creator-id="<%= comment.creator.id %>">
|
||||
<figure class="comment__avatar flex-item-no-shrink">
|
||||
<%= avatar_tag comment.creator, loading: :lazy %>
|
||||
</figure>
|
||||
|
||||
@@ -9,6 +9,10 @@
|
||||
<%= csrf_meta_tags %>
|
||||
<%= csp_meta_tag %>
|
||||
|
||||
<% if Current.user %>
|
||||
<meta name="current-user-id" content="<%= Current.user.id %>">
|
||||
<% end %>
|
||||
|
||||
<%= yield :head %>
|
||||
|
||||
<%= stylesheet_link_tag :all, "data-turbo-track": "reload" %>
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
class AddUniqueIndexToAssignments < ActiveRecord::Migration[8.0]
|
||||
def change
|
||||
add_index :assignments, %i[ assignee_id bubble_id ], unique: true
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,5 @@
|
||||
class MakeCommentBodiesNotNull < ActiveRecord::Migration[8.0]
|
||||
def change
|
||||
change_column_null :comments, :body, false
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,14 @@
|
||||
class CreateEvents < ActiveRecord::Migration[8.0]
|
||||
def change
|
||||
create_table :events do |t|
|
||||
t.references :bubble, null: false, index: false
|
||||
t.references :creator, null: false
|
||||
t.json :particulars, default: {}
|
||||
t.string :action, null: false
|
||||
|
||||
t.timestamps
|
||||
end
|
||||
|
||||
add_index :events, %i[ bubble_id action ], name: "index_events_on_bubble_id_and_action"
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,5 @@
|
||||
class DropBoosts < ActiveRecord::Migration[8.0]
|
||||
def change
|
||||
drop_table :boosts
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,5 @@
|
||||
class AddBoostCountToBubbles < ActiveRecord::Migration[8.0]
|
||||
def change
|
||||
add_column :bubbles, :boost_count, :integer, null: false, default: 0
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,5 @@
|
||||
class RemoveAssignmentsIndexOnAssigneeId < ActiveRecord::Migration[8.0]
|
||||
def change
|
||||
remove_index :assignments, :assignee_id
|
||||
end
|
||||
end
|
||||
Generated
+15
-11
@@ -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_02_163242) do
|
||||
ActiveRecord::Schema[8.0].define(version: 2024_10_09_211414) do
|
||||
create_table "accesses", force: :cascade do |t|
|
||||
t.integer "bucket_id", null: false
|
||||
t.integer "user_id", null: false
|
||||
@@ -64,18 +64,10 @@ ActiveRecord::Schema[8.0].define(version: 2024_10_02_163242) do
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.integer "assigner_id", null: false
|
||||
t.index ["assignee_id"], name: "index_assignments_on_assignee_id"
|
||||
t.index ["assignee_id", "bubble_id"], name: "index_assignments_on_assignee_id_and_bubble_id", unique: true
|
||||
t.index ["bubble_id"], name: "index_assignments_on_bubble_id"
|
||||
end
|
||||
|
||||
create_table "boosts", force: :cascade do |t|
|
||||
t.integer "creator_id", null: false
|
||||
t.integer "bubble_id", null: false
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.index ["bubble_id"], name: "index_boosts_on_bubble_id"
|
||||
end
|
||||
|
||||
create_table "bubbles", force: :cascade do |t|
|
||||
t.string "title"
|
||||
t.string "color"
|
||||
@@ -84,6 +76,7 @@ ActiveRecord::Schema[8.0].define(version: 2024_10_02_163242) do
|
||||
t.integer "creator_id", null: false
|
||||
t.date "due_on"
|
||||
t.integer "bucket_id", null: false
|
||||
t.integer "boost_count", default: 0, null: false
|
||||
t.index ["bucket_id"], name: "index_bubbles_on_bucket_id"
|
||||
end
|
||||
|
||||
@@ -98,13 +91,24 @@ ActiveRecord::Schema[8.0].define(version: 2024_10_02_163242) do
|
||||
end
|
||||
|
||||
create_table "comments", force: :cascade do |t|
|
||||
t.text "body"
|
||||
t.text "body", null: false
|
||||
t.integer "creator_id", null: false
|
||||
t.integer "bubble_id", null: false
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
end
|
||||
|
||||
create_table "events", force: :cascade do |t|
|
||||
t.integer "bubble_id", null: false
|
||||
t.integer "creator_id", null: false
|
||||
t.json "particulars", default: {}
|
||||
t.string "action", null: false
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.index ["bubble_id", "action"], name: "index_events_on_bubble_id_and_action"
|
||||
t.index ["creator_id"], name: "index_events_on_creator_id"
|
||||
end
|
||||
|
||||
create_table "pops", force: :cascade do |t|
|
||||
t.integer "bubble_id", null: false
|
||||
t.integer "user_id"
|
||||
|
||||
@@ -1,7 +1,15 @@
|
||||
require "test_helper"
|
||||
|
||||
class BoostsControllerTest < ActionDispatch::IntegrationTest
|
||||
# test "the truth" do
|
||||
# assert true
|
||||
# end
|
||||
setup do
|
||||
sign_in_as :kevin
|
||||
end
|
||||
|
||||
test "create" do
|
||||
assert_difference "bubbles(:logo).reload.boost_count", +1 do
|
||||
post bucket_bubble_boosts_url(buckets(:writebook), bubbles(:logo), format: :turbo_stream)
|
||||
end
|
||||
|
||||
assert_turbo_stream action: :update, target: dom_id(bubbles(:logo), :boosts)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,7 +1,15 @@
|
||||
require "test_helper"
|
||||
|
||||
class CommentsControllerTest < ActionDispatch::IntegrationTest
|
||||
# test "the truth" do
|
||||
# assert true
|
||||
# end
|
||||
setup do
|
||||
sign_in_as :kevin
|
||||
end
|
||||
|
||||
test "create" do
|
||||
assert_difference "bubbles(:logo).comments.count", +1 do
|
||||
post bucket_bubble_comments_url(buckets(:writebook), bubbles(:logo), params: { comment: { body: "Agreed." } })
|
||||
end
|
||||
|
||||
assert_redirected_to bucket_bubble_url(buckets(:writebook), bubbles(:logo))
|
||||
end
|
||||
end
|
||||
|
||||
Vendored
+7
@@ -1,7 +1,14 @@
|
||||
logo_jz:
|
||||
assigner: david
|
||||
assignee: jz
|
||||
bubble: logo
|
||||
created_at: <%= 1.week.ago %>
|
||||
|
||||
logo_kevin:
|
||||
assigner: david
|
||||
assignee: kevin
|
||||
bubble: logo
|
||||
created_at: <%= 1.day.ago %>
|
||||
|
||||
layout_jz:
|
||||
assigner: david
|
||||
|
||||
Vendored
-15
@@ -1,15 +0,0 @@
|
||||
logo_jz_one:
|
||||
bubble: logo
|
||||
creator: jz
|
||||
|
||||
logo_jz_two:
|
||||
bubble: logo
|
||||
creator: jz
|
||||
|
||||
logo_kevin:
|
||||
bubble: logo
|
||||
creator: kevin
|
||||
|
||||
layout_jz:
|
||||
bubble: layout
|
||||
creator: jz
|
||||
Vendored
+5
@@ -4,21 +4,26 @@ logo:
|
||||
title: The logo isn't big enough
|
||||
color: "#ED8008"
|
||||
due_on: <%= 3.days.from_now %>
|
||||
created_at: <%= 1.week.ago %>
|
||||
boost_count: 4
|
||||
|
||||
layout:
|
||||
bucket: writebook
|
||||
creator: david
|
||||
title: Layout is broken
|
||||
color: "#698F9C"
|
||||
created_at: <%= 1.week.ago %>
|
||||
|
||||
text:
|
||||
bucket: writebook
|
||||
creator: kevin
|
||||
title: The text is too small
|
||||
color: "#3B4B59"
|
||||
created_at: <%= 1.week.ago %>
|
||||
|
||||
shipping:
|
||||
bucket: writebook
|
||||
creator: kevin
|
||||
title: We need to ship the app
|
||||
color: "#ED8008"
|
||||
created_at: <%= 1.week.ago %>
|
||||
|
||||
Vendored
+1
@@ -2,6 +2,7 @@ logo_agreement_jz:
|
||||
body: I agree.
|
||||
creator: jz
|
||||
bubble: logo
|
||||
created_at: <%= 2.days.ago %>
|
||||
|
||||
logo_agreement_kevin:
|
||||
body: Same, let’s do it.
|
||||
|
||||
Vendored
+66
@@ -0,0 +1,66 @@
|
||||
logo_created:
|
||||
creator: david
|
||||
bubble: logo
|
||||
action: created
|
||||
created_at: <%= 1.week.ago %>
|
||||
|
||||
logo_assignment_jz:
|
||||
creator: david
|
||||
bubble: logo
|
||||
action: assigned
|
||||
particulars: <%= { assignee_ids: [ ActiveRecord::FixtureSet.identify(:jz) ] }.to_json %>
|
||||
created_at: <%= 1.week.ago %>
|
||||
|
||||
logo_assignment_km:
|
||||
creator: david
|
||||
bubble: logo
|
||||
action: assigned
|
||||
particulars: <%= { assignee_ids: [ ActiveRecord::FixtureSet.identify(:kevin) ] }.to_json %>
|
||||
created_at: <%= 1.day.ago %>
|
||||
|
||||
logo_boost_km1:
|
||||
creator: kevin
|
||||
bubble: logo
|
||||
action: boosted
|
||||
created_at: <%= 1.day.ago %>
|
||||
|
||||
logo_boost_km2:
|
||||
creator: kevin
|
||||
bubble: logo
|
||||
action: boosted
|
||||
created_at: <%= 1.day.ago %>
|
||||
|
||||
logo_boost_jz1:
|
||||
creator: jz
|
||||
bubble: logo
|
||||
action: boosted
|
||||
created_at: <%= 1.day.ago %>
|
||||
|
||||
logo_boost_jz2:
|
||||
creator: jz
|
||||
bubble: logo
|
||||
action: boosted
|
||||
|
||||
layout_created:
|
||||
creator: david
|
||||
bubble: layout
|
||||
action: created
|
||||
created_at: <%= 1.week.ago %>
|
||||
|
||||
layout_assignment_jz:
|
||||
creator: david
|
||||
bubble: layout
|
||||
action: assigned
|
||||
particulars: <%= { assignee_ids: [ ActiveRecord::FixtureSet.identify(:jz) ] }.to_json %>
|
||||
|
||||
text_created:
|
||||
creator: kevin
|
||||
bubble: text
|
||||
action: created
|
||||
created_at: <%= 1.week.ago %>
|
||||
|
||||
shipping_created:
|
||||
creator: kevin
|
||||
bubble: shipping
|
||||
action: created
|
||||
created_at: <%= 1.week.ago %>
|
||||
@@ -1,6 +1,31 @@
|
||||
require "test_helper"
|
||||
|
||||
class BubbleTest < ActiveSupport::TestCase
|
||||
setup do
|
||||
Current.user = users(:kevin)
|
||||
end
|
||||
|
||||
test "boosting" do
|
||||
assert_difference %w[ bubbles(:logo).boost_count bubbles(:logo).events.count ], +1 do
|
||||
bubbles(:logo).boost!
|
||||
end
|
||||
end
|
||||
|
||||
test "commenting" do
|
||||
bubbles(:logo).comment! "Agreed."
|
||||
|
||||
assert_equal "Agreed.", bubbles(:logo).comments.last.body
|
||||
end
|
||||
|
||||
test "assigning" do
|
||||
bubbles(:logo).assign users(:david)
|
||||
|
||||
assert_equal users(:kevin, :jz, :david), bubbles(:logo).assignees
|
||||
assert_equal users(:david, :kevin), bubbles(:logo).assigners.uniq
|
||||
assert_equal [ users(:david).id ], bubbles(:logo).events.last.assignee_ids
|
||||
assert_equal [ "David" ], bubbles(:logo).events.last.assignee_names
|
||||
end
|
||||
|
||||
test "searchable by title" do
|
||||
bubble = buckets(:writebook).bubbles.create! title: "Insufficient haggis", creator: users(:kevin)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user