diff --git a/app/controllers/bubbles/stage_pickers_controller.rb b/app/controllers/bubbles/stage_pickers_controller.rb new file mode 100644 index 000000000..3e7c0d25d --- /dev/null +++ b/app/controllers/bubbles/stage_pickers_controller.rb @@ -0,0 +1,21 @@ +class Bubbles::StagePickersController < ApplicationController + include BubbleScoped, BucketScoped + + before_action :set_workflows, :set_selected_workflow + + def new + end + + private + def set_workflows + @workflows = Current.account.workflows + end + + def set_selected_workflow + @selected_workflow = if params[:workflow_id] + @workflows.find params[:workflow_id] + else + @bubble.workflow || @workflows.first + end + end +end diff --git a/app/controllers/bubbles/stagings_controller.rb b/app/controllers/bubbles/stagings_controller.rb new file mode 100644 index 000000000..3a17b169c --- /dev/null +++ b/app/controllers/bubbles/stagings_controller.rb @@ -0,0 +1,8 @@ +class Bubbles::StagingsController < ApplicationController + include BubbleScoped, BucketScoped + + def create + @bubble.toggle_stage Current.account.stages.find(params[:stage_id]) + redirect_to new_bucket_bubble_stage_picker_path(@bucket, @bubble) + end +end diff --git a/app/controllers/concerns/.keep b/app/controllers/concerns/.keep deleted file mode 100644 index e69de29bb..000000000 diff --git a/app/controllers/concerns/workflow_scoped.rb b/app/controllers/concerns/workflow_scoped.rb new file mode 100644 index 000000000..ca2e7398c --- /dev/null +++ b/app/controllers/concerns/workflow_scoped.rb @@ -0,0 +1,12 @@ +module WorkflowScoped + extend ActiveSupport::Concern + + included do + before_action :set_workflow + end + + private + def set_workflow + @workflow = Current.account.workflows.find params[:workflow_id] + end +end diff --git a/app/controllers/workflows/stages_controller.rb b/app/controllers/workflows/stages_controller.rb new file mode 100644 index 000000000..d05ae61c4 --- /dev/null +++ b/app/controllers/workflows/stages_controller.rb @@ -0,0 +1,36 @@ +class Workflows::StagesController < ApplicationController + include WorkflowScoped + + before_action :set_stage, only: %i[ edit update destroy ] + + def new + @stage = @workflow.stages.new + end + + def create + @stage = @workflow.stages.create! stage_params + redirect_to account_workflow_path(@workflow) + end + + def edit + end + + def update + @stage.update! stage_params + redirect_to account_workflow_path(@workflow) + end + + def destroy + @stage.destroy + redirect_to account_workflow_path(@workflow) + end + + private + def set_stage + @stage = @workflow.stages.find params[:id] + end + + def stage_params + params.require(:workflow_stage).permit(:name) + end +end diff --git a/app/controllers/workflows_controller.rb b/app/controllers/workflows_controller.rb new file mode 100644 index 000000000..95117f9b9 --- /dev/null +++ b/app/controllers/workflows_controller.rb @@ -0,0 +1,43 @@ +class WorkflowsController < ApplicationController + before_action :set_workflow, only: %i[ show edit update destroy ] + + def index + @workflows = Current.account.workflows + end + + def new + @workflow = Current.account.workflows.new + end + + def create + @workflow = Current.account.workflows.create! workflow_params + # FIXME: this should definitely change. + %w[ Triage WIP On-hold ].each { |name| @workflow.stages.create! name: name } + redirect_to account_workflows_path + end + + def show + end + + def edit + end + + def update + @workflow.update! workflow_params + redirect_to account_workflow_path(@workflow) + end + + def destroy + @workflow.destroy + redirect_to account_workflows_path + end + + private + def set_workflow + @workflow = Current.account.workflows.find params[:id] + end + + def workflow_params + params.require(:workflow).permit(:name) + end +end diff --git a/app/helpers/workflows_helper.rb b/app/helpers/workflows_helper.rb new file mode 100644 index 000000000..80297ce12 --- /dev/null +++ b/app/helpers/workflows_helper.rb @@ -0,0 +1,10 @@ +module WorkflowsHelper + def link_to_stage_picker(bubble, workflow) + link_to workflow.name, new_bucket_bubble_stage_picker_path(bubble.bucket, bubble, workflow_id: workflow.id) + end + + def button_to_set_stage(bubble, stage) + button_to stage.name, bucket_bubble_stagings_path(bubble.bucket, bubble, stage_id: stage.id), + method: :post, class: [ "btn btn--small", { "fill-selected": stage == bubble.stage } ] + end +end diff --git a/app/models/account.rb b/app/models/account.rb index ec9290b11..101c96210 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -6,5 +6,8 @@ class Account < ApplicationRecord has_many :buckets, dependent: :destroy has_many :bubbles, through: :buckets + has_many :workflows, dependent: :destroy + has_many :stages, through: :workflows, class_name: "Workflow::Stage" + has_many :tags, dependent: :destroy end diff --git a/app/models/bubble.rb b/app/models/bubble.rb index e2281f617..3f7345469 100644 --- a/app/models/bubble.rb +++ b/app/models/bubble.rb @@ -1,5 +1,5 @@ class Bubble < ApplicationRecord - include Assignable, Boostable, Colored, Commentable, Eventable, Poppable, Searchable, Taggable, Threaded + include Assignable, Boostable, Colored, Commentable, Eventable, Poppable, Searchable, Staged, Taggable, Threaded belongs_to :bucket belongs_to :creator, class_name: "User", default: -> { Current.user } diff --git a/app/models/bubble/staged.rb b/app/models/bubble/staged.rb new file mode 100644 index 000000000..39e94f632 --- /dev/null +++ b/app/models/bubble/staged.rb @@ -0,0 +1,21 @@ +module Bubble::Staged + extend ActiveSupport::Concern + + included do + belongs_to :stage, class_name: "Workflow::Stage", optional: true + end + + def workflow + stage&.workflow + end + + def toggle_stage(stage) + if self.stage == stage + update! stage: nil + track_event :unstaged, stage_id: stage.id + else + update! stage: stage + track_event :staged, stage_id: stage.id + end + end +end diff --git a/app/models/bubble/thread/rollup.rb b/app/models/bubble/thread/rollup.rb index dd034aa8b..daa25c32b 100644 --- a/app/models/bubble/thread/rollup.rb +++ b/app/models/bubble/thread/rollup.rb @@ -25,9 +25,10 @@ class Bubble::Thread::Rollup 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 ] + when "created" then [ 1, entry.created_at ] + when "assigned" then [ 2, entry.created_at ] + when "staged", "unstaged" then [ 3, entry.created_at ] + when "boosted" then [ 4, entry.creator, entry.created_at ] end end end @@ -41,9 +42,13 @@ class Bubble::Thread::Rollup 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 = "assigned to #{entry.assignees.map(&:name).to_sentence}" summary += " #{time_ago_in_words(entry.created_at)} ago" unless first_position? summary + when "staged" + "#{entry.creator.name} moved this to '#{entry.stage.name}'" + when "unstaged" + "#{entry.creator.name} removed this from '#{entry.stage.name}'" when "boosted" "#{entry.creator.name} +#{chunk_size}" end diff --git a/app/models/event.rb b/app/models/event.rb index 119d871ac..7946e79fe 100644 --- a/app/models/event.rb +++ b/app/models/event.rb @@ -1,7 +1,7 @@ class Event < ApplicationRecord - THREADABLE_ACTIONS = %w[ assigned boosted created ] + THREADABLE_ACTIONS = %w[ assigned boosted created staged unstaged ] - include Assignments + include Assignments, Stages belongs_to :creator, class_name: "User" belongs_to :bubble, touch: true diff --git a/app/models/event/assignments.rb b/app/models/event/assignments.rb index 1daedbf9f..56a6b9e6e 100644 --- a/app/models/event/assignments.rb +++ b/app/models/event/assignments.rb @@ -5,12 +5,7 @@ module Event::Assignments store_accessor :particulars, :assignee_ids end - def assignee_names - assignees.map &:name + def assignees + @assignees ||= account.users.where id: assignee_ids end - - private - def assignees - @assignees ||= account.users.where id: assignee_ids - end end diff --git a/app/models/event/stages.rb b/app/models/event/stages.rb new file mode 100644 index 000000000..65b20e3ad --- /dev/null +++ b/app/models/event/stages.rb @@ -0,0 +1,11 @@ +module Event::Stages + extend ActiveSupport::Concern + + included do + store_accessor :particulars, :stage_id + end + + def stage + @stage ||= account.stages.find_by_id stage_id + end +end diff --git a/app/models/workflow.rb b/app/models/workflow.rb new file mode 100644 index 000000000..164d3eb47 --- /dev/null +++ b/app/models/workflow.rb @@ -0,0 +1,4 @@ +class Workflow < ApplicationRecord + belongs_to :account + has_many :stages, dependent: :delete_all +end diff --git a/app/models/workflow/stage.rb b/app/models/workflow/stage.rb new file mode 100644 index 000000000..9eb7ef1d0 --- /dev/null +++ b/app/models/workflow/stage.rb @@ -0,0 +1,3 @@ +class Workflow::Stage < ApplicationRecord + belongs_to :workflow +end diff --git a/app/views/accounts/users/index.html.erb b/app/views/accounts/users/index.html.erb index dea54bb15..1c707e96c 100644 --- a/app/views/accounts/users/index.html.erb +++ b/app/views/accounts/users/index.html.erb @@ -21,3 +21,8 @@
<%= render partial: "accounts/users/user", collection: @users %>
+ +
+

Workflows

+ <%= turbo_frame_tag :workflows, src: account_workflows_path %> +
diff --git a/app/views/bubbles/_pop_toggle.html.erb b/app/views/bubbles/_pop_toggle.html.erb new file mode 100644 index 000000000..146ea5aee --- /dev/null +++ b/app/views/bubbles/_pop_toggle.html.erb @@ -0,0 +1,11 @@ +<% if bubble.popped? %> + <%= button_to bucket_bubble_pop_path(bubble.bucket, bubble), method: :delete, class: "btn" do %> + <%= image_tag "pop.svg", aria: { hidden: true }, size: 24 %> + Un-pop + <% end %> +<% else %> + <%= button_to bucket_bubble_pop_path(bubble.bucket, bubble), class: "btn" do %> + <%= image_tag "pop.svg", aria: { hidden: true }, size: 24 %> + Pop + <% end %> +<% end %> diff --git a/app/views/bubbles/filters/_bucket_view_form.html.erb b/app/views/bubbles/filters/_bucket_view_form.html.erb index 56d911e9b..a910a6fb9 100644 --- a/app/views/bubbles/filters/_bucket_view_form.html.erb +++ b/app/views/bubbles/filters/_bucket_view_form.html.erb @@ -18,8 +18,8 @@ Update <% end %> - <%= button_tag type: :submit, form: dom_id(view, :delete_form), class: "btn", style: "font-size: 0.4em;" do %> - <%= image_tag "alert.svg", aria: { hidden: true }, size: 24 %> + <%= button_tag type: :submit, form: dom_id(view, :delete_form), class: "btn", style: "font-size: 0.4em;", data: { turbo_confirm: "Are you sure you want to delete this filter?" } do %> + <%= image_tag "minus.svg", aria: { hidden: true }, size: 24 %> Delete <% end %> <% end %> diff --git a/app/views/bubbles/show.html.erb b/app/views/bubbles/show.html.erb index 9d320c4f7..a43755cd0 100644 --- a/app/views/bubbles/show.html.erb +++ b/app/views/bubbles/show.html.erb @@ -7,18 +7,11 @@ All Bubbles <% end %> - <% if @bubble.popped? %> - <%= button_to bucket_bubble_pop_path(@bubble.bucket, @bubble), method: :delete, class: "btn" do %> - <%= image_tag "pop.svg", aria: { hidden: true }, size: 24 %> - Un-pop - <% end %> - <% else %> - <%= button_to bucket_bubble_pop_path(@bubble.bucket, @bubble), class: "btn" do %> - <%= image_tag "pop.svg", aria: { hidden: true }, size: 24 %> - Pop - <% end %> - <% end %> - +
+ <%= render "bubbles/pop_toggle", bubble: @bubble %> + <%= turbo_frame_tag dom_id(@bubble, :stage_picker), src: new_bucket_bubble_stage_picker_path(@bubble.bucket, @bubble) %> +
+ <% end %>
diff --git a/app/views/bubbles/stage_pickers/new.html.erb b/app/views/bubbles/stage_pickers/new.html.erb new file mode 100644 index 000000000..a0ee840b1 --- /dev/null +++ b/app/views/bubbles/stage_pickers/new.html.erb @@ -0,0 +1,25 @@ +<%= turbo_frame_tag dom_id(@bubble, :stage_picker) do %> + <% if @selected_workflow %> +
+ + + + + <% @workflows.each do |workflow| %> +
  • <%= link_to_stage_picker @bubble, workflow %>
  • + <% end %> +
    +
    + +
    + + <% @selected_workflow.stages.each do |stage| %> +
  • <%= button_to_set_stage @bubble, stage %>
  • + <% end %> +
    +
    +
    + <% end %> +<% end %> diff --git a/app/views/workflows/_workflow.html.erb b/app/views/workflows/_workflow.html.erb new file mode 100644 index 000000000..ab7f7272b --- /dev/null +++ b/app/views/workflows/_workflow.html.erb @@ -0,0 +1,26 @@ +<%= turbo_frame_tag workflow do %> +
    +
    + <%= turbo_frame_tag dom_id(workflow, :header) do %> +

    <%= workflow.name %>

    + +
    + <%= link_to edit_account_workflow_path(workflow), class: "btn btn--small" do %> + <%= image_tag "pencil.svg", aria: { hidden: true }, size: 24 %> + <% end %> + + <%= button_to account_workflow_path(workflow), method: :delete, class: "btn btn--small", data: { turbo_frame: :workflows, turbo_confirm: "Are you sure you want to delete this workflow?" } do %> + <%= image_tag "minus.svg", aria: { hidden: true }, size: 24 %> + <% end %> +
    + <% end %> +
    + +
    + <%= turbo_frame_tag dom_id(workflow, :stages) do %> + <%= render partial: "workflows/stages/stage", collection: workflow.stages %> + <%= link_to "New stage", new_account_workflow_stage_path(workflow), class: "btn btn--small" %> + <% end %> +
    +
    +<% end %> diff --git a/app/views/workflows/edit.html.erb b/app/views/workflows/edit.html.erb new file mode 100644 index 000000000..292802c5b --- /dev/null +++ b/app/views/workflows/edit.html.erb @@ -0,0 +1,17 @@ +<%= turbo_frame_tag dom_id(@workflow, :header) do %> + <%= form_with model: @workflow, url: account_workflow_path(@workflow) do |form| %> +
    + <%= form.text_field :name, required: true, autofocus: true %> + +
    + <%= form.button type: :submit, class: "btn btn--small" do %> + <%= image_tag "check.svg", aria: { hidden: true }, size: 24 %> + <% end %> + + <%= link_to account_workflow_path(@workflow), class: "btn btn--small", data: { turbo_frame: dom_id(@workflow), turbo_confirm: "Are you sure you want to delete this workflow?" } do %> + <%= image_tag "remove.svg", aria: { hidden: true }, size: 24 %> + <% end %> +
    +
    + <% end %> +<% end %> diff --git a/app/views/workflows/index.html.erb b/app/views/workflows/index.html.erb new file mode 100644 index 000000000..31be2970c --- /dev/null +++ b/app/views/workflows/index.html.erb @@ -0,0 +1,7 @@ +<%= turbo_frame_tag :workflows do %> +
    + <%= render partial: "workflows/workflow", collection: @workflows %> +
    + + <%= link_to "New workflow", new_account_workflow_path, class: "btn" %> +<% end %> diff --git a/app/views/workflows/new.html.erb b/app/views/workflows/new.html.erb new file mode 100644 index 000000000..7ebe61b49 --- /dev/null +++ b/app/views/workflows/new.html.erb @@ -0,0 +1,6 @@ +<%= turbo_frame_tag :workflows do %> + <%= form_with model: @workflow, url: account_workflows_path do |form| %> + <%= form.text_field :name, required: true, autofocus: true %> + <%= form.submit %> + <% end %> +<% end %> diff --git a/app/views/workflows/show.html.erb b/app/views/workflows/show.html.erb new file mode 100644 index 000000000..17ccd8193 --- /dev/null +++ b/app/views/workflows/show.html.erb @@ -0,0 +1 @@ +<%= render "workflows/workflow", workflow: @workflow %> diff --git a/app/views/workflows/stages/_stage.html.erb b/app/views/workflows/stages/_stage.html.erb new file mode 100644 index 000000000..fb8119441 --- /dev/null +++ b/app/views/workflows/stages/_stage.html.erb @@ -0,0 +1,17 @@ +
    + <%= turbo_frame_tag stage do %> +

    <%= stage.name %>

    + +
    + <%= link_to edit_account_workflow_stage_path(stage.workflow, stage), class: "btn btn--small" do %> + <%= image_tag "pencil.svg", aria: { hidden: true }, size: 24 %> + <% end %> + + <% if stage_counter >= 1 %> + <%= button_to account_workflow_stage_path(stage.workflow, stage), method: :delete, class: "btn btn--small", data: { turbo_frame: dom_id(stage.workflow), turbo_confirm: "Are you sure you want to delete this stage?" } do %> + <%= image_tag "minus.svg", aria: { hidden: true }, size: 24 %> + <% end %> + <% end %> +
    + <% end %> +
    diff --git a/app/views/workflows/stages/edit.html.erb b/app/views/workflows/stages/edit.html.erb new file mode 100644 index 000000000..5b7d7e259 --- /dev/null +++ b/app/views/workflows/stages/edit.html.erb @@ -0,0 +1,17 @@ +<%= turbo_frame_tag @stage do %> + <%= form_with model: @stage, url: account_workflow_stage_path(@stage.workflow, @stage) do |form| %> +
    + <%= form.text_field :name, required: true, autofocus: true %> + +
    + <%= form.button type: :submit, class: "btn btn--small" do %> + <%= image_tag "check.svg", aria: { hidden: true }, size: 24 %> + <% end %> + + <%= link_to account_workflow_path(@workflow), class: "btn btn--small", data: { turbo_frame: dom_id(@workflow), turbo_confirm: "Are you sure you want to delete this workflow?" } do %> + <%= image_tag "remove.svg", aria: { hidden: true }, size: 24 %> + <% end %> +
    +
    + <% end %> +<% end %> diff --git a/app/views/workflows/stages/new.html.erb b/app/views/workflows/stages/new.html.erb new file mode 100644 index 000000000..4a479df4f --- /dev/null +++ b/app/views/workflows/stages/new.html.erb @@ -0,0 +1,6 @@ +<%= turbo_frame_tag dom_id(@stage.workflow, :stages) do %> + <%= form_with model: @stage, url: account_workflow_stages_path(@stage.workflow) do |form| %> + <%= form.text_field :name, required: true, autofocus: true %> + <%= form.submit %> + <% end %> +<% end %> diff --git a/config/routes.rb b/config/routes.rb index 4286d0fa8..a29ef9879 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -8,28 +8,30 @@ Rails.application.routes.draw do end end + resolve "Bubble" do |bubble, options| + route_for :bucket_bubble, bubble.bucket, bubble, options + end + resources :buckets do - scope module: :buckets do - resources :views - end - resources :bubbles do - scope module: :bubbles do - resource :image - resource :pop - end - resources :assignments resources :boosts resources :comments resources :tags, shallow: true + + scope module: :bubbles do + resource :image + resource :pop + resource :stage_picker + resources :stagings + end end resources :tags, only: :index - end - resolve "Bubble" do |bubble, options| - route_for :bucket_bubble, bubble.bucket, bubble, options + scope module: :buckets do + resources :views + end end resource :first_run @@ -41,6 +43,10 @@ Rails.application.routes.draw do end end + resources :workflows do + resources :stages, module: :workflows + 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 diff --git a/db/migrate/20241018175019_create_workflows.rb b/db/migrate/20241018175019_create_workflows.rb new file mode 100644 index 000000000..9d57fc8fb --- /dev/null +++ b/db/migrate/20241018175019_create_workflows.rb @@ -0,0 +1,10 @@ +class CreateWorkflows < ActiveRecord::Migration[8.0] + def change + create_table :workflows do |t| + t.references :account, null: false, foreign_key: true + t.string :name, null: false + + t.timestamps + end + end +end diff --git a/db/migrate/20241018175057_create_workflow_stages.rb b/db/migrate/20241018175057_create_workflow_stages.rb new file mode 100644 index 000000000..ff7d3e490 --- /dev/null +++ b/db/migrate/20241018175057_create_workflow_stages.rb @@ -0,0 +1,10 @@ +class CreateWorkflowStages < ActiveRecord::Migration[8.0] + def change + create_table :workflow_stages do |t| + t.references :workflow, null: false, foreign_key: true + t.string :name, null: false + + t.timestamps + end + end +end diff --git a/db/migrate/20241018223027_add_workflow_stage_to_bubbles.rb b/db/migrate/20241018223027_add_workflow_stage_to_bubbles.rb new file mode 100644 index 000000000..cb528612a --- /dev/null +++ b/db/migrate/20241018223027_add_workflow_stage_to_bubbles.rb @@ -0,0 +1,5 @@ +class AddWorkflowStageToBubbles < ActiveRecord::Migration[8.0] + def change + add_reference :bubbles, :stage, null: true, foreign_key: { to_table: :workflow_stages } + end +end diff --git a/db/schema.rb b/db/schema.rb index 77284d456..49c2ebde3 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -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_17_202003) do +ActiveRecord::Schema[8.0].define(version: 2024_10_18_223027) do create_table "accesses", force: :cascade do |t| t.integer "bucket_id", null: false t.integer "user_id", null: false @@ -77,7 +77,9 @@ ActiveRecord::Schema[8.0].define(version: 2024_10_17_202003) do t.date "due_on" t.integer "bucket_id", null: false t.integer "boost_count", default: 0, null: false + t.integer "stage_id" t.index ["bucket_id"], name: "index_bubbles_on_bucket_id" + t.index ["stage_id"], name: "index_bubbles_on_stage_id" end create_table "bucket_views", force: :cascade do |t| @@ -166,14 +168,33 @@ ActiveRecord::Schema[8.0].define(version: 2024_10_17_202003) do t.index ["email_address"], name: "index_users_on_email_address", unique: true end + create_table "workflow_stages", force: :cascade do |t| + t.integer "workflow_id", null: false + t.string "name", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["workflow_id"], name: "index_workflow_stages_on_workflow_id" + end + + create_table "workflows", force: :cascade do |t| + t.integer "account_id", null: false + t.string "name", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["account_id"], name: "index_workflows_on_account_id" + end + add_foreign_key "active_storage_attachments", "active_storage_blobs", column: "blob_id" add_foreign_key "active_storage_variant_records", "active_storage_blobs", column: "blob_id" + add_foreign_key "bubbles", "workflow_stages", column: "stage_id" add_foreign_key "pops", "bubbles" add_foreign_key "pops", "users" add_foreign_key "sessions", "users" add_foreign_key "taggings", "bubbles" add_foreign_key "taggings", "tags" add_foreign_key "users", "accounts" + add_foreign_key "workflow_stages", "workflows" + add_foreign_key "workflows", "accounts" # Virtual tables defined in this database. # Note that virtual tables may not work with other database engines. Be careful if changing database. diff --git a/test/models/bubble_test.rb b/test/models/bubble_test.rb index 4ffe9d923..e8c11a9ec 100644 --- a/test/models/bubble_test.rb +++ b/test/models/bubble_test.rb @@ -17,7 +17,7 @@ class BubbleTest < ActiveSupport::TestCase 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 + assert_equal [ "David" ], bubbles(:logo).events.last.assignees.map(&:name) end test "searchable by title" do