diff --git a/Gemfile b/Gemfile
index 9f98d3b0d..c70e1eb17 100644
--- a/Gemfile
+++ b/Gemfile
@@ -8,6 +8,7 @@ gem "importmap-rails"
gem "propshaft"
gem "stimulus-rails"
gem "turbo-rails"
+gem "hotwire_combobox", github: "josefarias/hotwire_combobox", branch: :main
# Deployment and drivers
gem "bootsnap", require: false
diff --git a/Gemfile.lock b/Gemfile.lock
index d15178a1e..fccbac836 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -1,3 +1,14 @@
+GIT
+ remote: https://github.com/josefarias/hotwire_combobox.git
+ revision: 57ee6c9087320fab9383a07fa919fe75e0ea72ba
+ branch: main
+ specs:
+ hotwire_combobox (0.3.2)
+ platform_agent (>= 1.0.1)
+ rails (>= 7.0.7.2)
+ stimulus-rails (>= 1.2)
+ turbo-rails (>= 1.2)
+
GEM
remote: https://rubygems.org/
specs:
@@ -157,6 +168,9 @@ GEM
parser (3.3.5.0)
ast (~> 2.4.1)
racc
+ platform_agent (1.0.1)
+ activesupport (>= 5.2.0)
+ useragent (~> 0.16.3)
propshaft (1.1.0)
actionpack (>= 7.0.0)
activesupport (>= 7.0.0)
@@ -319,6 +333,7 @@ DEPENDENCIES
brakeman
capybara
debug
+ hotwire_combobox!
importmap-rails
propshaft
puma (>= 5.0)
diff --git a/app/assets/stylesheets/bubbles.css b/app/assets/stylesheets/bubbles.css
index c180524ec..cde38c362 100644
--- a/app/assets/stylesheets/bubbles.css
+++ b/app/assets/stylesheets/bubbles.css
@@ -155,6 +155,10 @@
}
}
+ &:focus-within {
+ z-index: 2;
+ }
+
select {
inset: 0;
position: absolute;
diff --git a/app/assets/stylesheets/comboboxes.css b/app/assets/stylesheets/comboboxes.css
new file mode 100644
index 000000000..3cfce33c9
--- /dev/null
+++ b/app/assets/stylesheets/comboboxes.css
@@ -0,0 +1,33 @@
+:root {
+ --hw-option-bg-color: var(--color-ink-reversed);
+ --hw-active-bg-color: var(--color-selected);
+ --hw-border-color: var(--color-subtle-dark);
+ --hw-border-radius: 1em;
+}
+
+.hw-combobox {
+ turbo-frame {
+ display: inline;
+ }
+
+ .hw-combobox__main__wrapper {
+ background-color: var(--color-ink-reversed);
+ }
+
+ .hw-combobox__input {
+ background-color: var(--color-ink-reversed);
+ }
+}
+
+.bubble__meta {
+ .hw-combobox {
+ position: absolute;
+ right: 0;
+ top: -0.25rem;
+ z-index: 1;
+ }
+
+ .hw-combobox__main__wrapper {
+ position: absolute;
+ }
+}
diff --git a/app/controllers/assignments/swaps_controller.rb b/app/controllers/assignments/swaps_controller.rb
new file mode 100644
index 000000000..3554a1369
--- /dev/null
+++ b/app/controllers/assignments/swaps_controller.rb
@@ -0,0 +1,17 @@
+class Assignments::SwapsController < ApplicationController
+ include BubbleScoped, BucketScoped
+
+ def create
+ @bubble.swap_assignment incoming_assignee, outgoing_assignee
+ redirect_to @bubble
+ end
+
+ private
+ def incoming_assignee
+ @bucket.users.active.find params.expect(:incoming_assignee_id)
+ end
+
+ def outgoing_assignee
+ @bucket.users.active.find params.expect(:outgoing_assignee_id)
+ end
+end
diff --git a/app/controllers/assignments/toggles_controller.rb b/app/controllers/assignments/toggles_controller.rb
new file mode 100644
index 000000000..bcf1616be
--- /dev/null
+++ b/app/controllers/assignments/toggles_controller.rb
@@ -0,0 +1,13 @@
+class Assignments::TogglesController < ApplicationController
+ include BubbleScoped, BucketScoped
+
+ def create
+ @bubble.toggle_assignment assignee
+ redirect_to @bubble
+ end
+
+ private
+ def assignee
+ @bucket.users.active.find params.expect(:assignee_id)
+ end
+end
diff --git a/app/controllers/assignments_controller.rb b/app/controllers/assignments_controller.rb
deleted file mode 100644
index 872725b53..000000000
--- a/app/controllers/assignments_controller.rb
+++ /dev/null
@@ -1,19 +0,0 @@
-class AssignmentsController < ApplicationController
- include BubbleScoped, BucketScoped
-
- def new
- end
-
- def show
- end
-
- def create
- @bubble.assign(find_assignee)
- redirect_to @bubble
- end
-
- private
- def find_assignee
- @bucket.users.active.find(params.expect(:assignee_id))
- end
-end
diff --git a/app/controllers/bubbles/tags_controller.rb b/app/controllers/bubbles/tags_controller.rb
new file mode 100644
index 000000000..4ac791ae0
--- /dev/null
+++ b/app/controllers/bubbles/tags_controller.rb
@@ -0,0 +1,7 @@
+class Bubbles::TagsController < ApplicationController
+ include BubbleScoped, BucketScoped
+
+ def index
+ @tags = Current.account.tags.search params[:q]
+ end
+end
diff --git a/app/controllers/bubbles/users_controller.rb b/app/controllers/bubbles/users_controller.rb
new file mode 100644
index 000000000..23b8b9009
--- /dev/null
+++ b/app/controllers/bubbles/users_controller.rb
@@ -0,0 +1,7 @@
+class Bubbles::UsersController < ApplicationController
+ include BubbleScoped, BucketScoped
+
+ def index
+ @users = @bucket.users.active.search params[:q]
+ end
+end
diff --git a/app/controllers/taggings/swaps_controller.rb b/app/controllers/taggings/swaps_controller.rb
new file mode 100644
index 000000000..b335c1a6c
--- /dev/null
+++ b/app/controllers/taggings/swaps_controller.rb
@@ -0,0 +1,21 @@
+class Taggings::SwapsController < ApplicationController
+ include BubbleScoped, BucketScoped
+
+ def create
+ @bubble.swap_tag incoming_tag, outgoing_tag
+ redirect_to @bubble
+ end
+
+ private
+ def incoming_tag
+ if params[:incoming_tag_title]
+ Tag.new title: params[:incoming_tag_title]
+ else
+ Current.account.tags.find params.expect(:incoming_tag_id)
+ end
+ end
+
+ def outgoing_tag
+ Current.account.tags.find params.expect(:outgoing_tag_id)
+ end
+end
diff --git a/app/controllers/taggings/toggles_controller.rb b/app/controllers/taggings/toggles_controller.rb
new file mode 100644
index 000000000..f1b611eb7
--- /dev/null
+++ b/app/controllers/taggings/toggles_controller.rb
@@ -0,0 +1,17 @@
+class Taggings::TogglesController < ApplicationController
+ include BubbleScoped, BucketScoped
+
+ def create
+ @bubble.toggle_tag tag
+ redirect_to @bubble
+ end
+
+ private
+ def tag
+ if params[:tag_title]
+ Tag.new title: params[:tag_title]
+ else
+ Current.account.tags.find params.expect(:tag_id)
+ end
+ end
+end
diff --git a/app/controllers/tags_controller.rb b/app/controllers/tags_controller.rb
deleted file mode 100644
index 275ffce05..000000000
--- a/app/controllers/tags_controller.rb
+++ /dev/null
@@ -1,23 +0,0 @@
-class TagsController < ApplicationController
- include BucketScoped
-
- before_action :set_bubble, only: %i[ new create ]
-
- def index
- @tags = Current.account.tags.order(:title)
- end
-
- def new
- end
-
- # FIXME: Should move this to a taggings controller to separate tag administration from applying
- def create
- @bubble.tag(params.require(:tag).expect(:title))
- redirect_to @bubble
- end
-
- private
- def set_bubble
- @bubble = @bucket.bubbles.find(params[:bubble_id])
- end
-end
diff --git a/app/javascript/controllers/autofocus_controller.js b/app/javascript/controllers/autofocus_controller.js
new file mode 100644
index 000000000..75e5c4de9
--- /dev/null
+++ b/app/javascript/controllers/autofocus_controller.js
@@ -0,0 +1,16 @@
+import { Controller } from "@hotwired/stimulus"
+
+export default class extends Controller {
+ static targets = [ "element" ]
+
+ connect() {
+ this.observer = new IntersectionObserver(([entry]) => {
+ if (entry.isIntersecting) this.elementTarget.focus()
+ })
+ this.observer.observe(this.elementTarget)
+ }
+
+ disconnect() {
+ this.observer.disconnect()
+ }
+}
diff --git a/app/javascript/controllers/expandable_controller.js b/app/javascript/controllers/expandable_controller.js
new file mode 100644
index 000000000..74b3bc050
--- /dev/null
+++ b/app/javascript/controllers/expandable_controller.js
@@ -0,0 +1,9 @@
+import { Controller } from "@hotwired/stimulus"
+
+export default class extends Controller {
+ closeOnFocusOutside(event) {
+ if (!this.element.contains(event.relatedTarget)) {
+ this.element.removeAttribute("open")
+ }
+ }
+}
diff --git a/app/models/assignment.rb b/app/models/assignment.rb
index 54abe682e..7233030b3 100644
--- a/app/models/assignment.rb
+++ b/app/models/assignment.rb
@@ -3,6 +3,4 @@ class Assignment < ApplicationRecord
belongs_to :assignee, class_name: "User"
belongs_to :assigner, class_name: "User"
-
- validates :assignee, uniqueness: { scope: :bubble }
end
diff --git a/app/models/bubble/assignable.rb b/app/models/bubble/assignable.rb
index 0b6dc3f9a..5bcd241fd 100644
--- a/app/models/bubble/assignable.rb
+++ b/app/models/bubble/assignable.rb
@@ -10,12 +10,30 @@ module Bubble::Assignable
scope :assigned_by, ->(users) { joins(:assignments).where(assignments: { assigner: users }).distinct }
end
- def assign(users, assigner: Current.user)
- assignee_rows = Array(users).collect { |user| { assignee_id: user.id, assigner_id: assigner.id, bubble_id: id } }
+ def assign(user, assigner: Current.user)
+ assignments.create! assignee: user, assigner: assigner
+ track_event :assigned, assignee_ids: [ user.id ]
+ rescue ActiveRecord::RecordNotUnique
+ # Already assigned
+ end
+ def unassign(user)
+ destructions = assignments.destroy_by assignee: user
+ track_event :unassigned, assignee_ids: [ user.id ] if destructions.any?
+ end
+
+ def swap_assignment(incoming, outgoing)
transaction do
- Assignment.insert_all assignee_rows
- track_event :assigned, assignee_ids: assignee_rows.pluck(:assignee_id)
+ unassign outgoing
+ assign incoming unless incoming == outgoing
end
end
+
+ def toggle_assignment(user)
+ assigned_to?(user) ? unassign(user) : assign(user)
+ end
+
+ def assigned_to?(user)
+ assignments.exists? assignee: user
+ end
end
diff --git a/app/models/bubble/taggable.rb b/app/models/bubble/taggable.rb
index 90bef0eb2..d32e58a27 100644
--- a/app/models/bubble/taggable.rb
+++ b/app/models/bubble/taggable.rb
@@ -8,7 +8,28 @@ module Bubble::Taggable
scope :tagged_with, ->(tags) { joins(:taggings).where(taggings: { tag: tags }) }
end
- def tag(title)
- taggings.create! tag: bucket.account.tags.find_or_create_by!(title: title)
+ def tag(tag)
+ taggings.create! tag: tag
+ rescue ActiveRecord::RecordNotUnique
+ # Already tagged
+ end
+
+ def untag(tag)
+ taggings.destroy_by tag: tag
+ end
+
+ def swap_tag(incoming, outgoing)
+ transaction do
+ untag outgoing
+ tag incoming unless incoming == outgoing
+ end
+ end
+
+ def toggle_tag(tag)
+ tagged_with?(tag) ? untag(tag) : tag(tag)
+ end
+
+ def tagged_with?(tag)
+ tags.include? tag
end
end
diff --git a/app/models/event_summary.rb b/app/models/event_summary.rb
index de90ff447..2c1d23b07 100644
--- a/app/models/event_summary.rb
+++ b/app/models/event_summary.rb
@@ -21,6 +21,8 @@ class EventSummary < ApplicationRecord
"Added by #{event.creator.name} #{time_ago_in_words(event.created_at)} ago."
when "assigned"
"Assigned to #{event.assignees.pluck(:name).to_sentence} #{time_ago_in_words(event.created_at)} ago."
+ when "unassigned"
+ "Unassigned from #{event.assignees.pluck(:name).to_sentence} #{time_ago_in_words(event.created_at)} ago."
when "staged"
"#{event.creator.name} moved this to '#{event.stage_name}'."
when "unstaged"
diff --git a/app/models/tag.rb b/app/models/tag.rb
index c4cd4209e..5937ea8cd 100644
--- a/app/models/tag.rb
+++ b/app/models/tag.rb
@@ -1,12 +1,18 @@
class Tag < ApplicationRecord
include Filterable
- belongs_to :account
+ belongs_to :account, default: -> { Current.account }
has_many :taggings, dependent: :destroy
has_many :bubbles, through: :taggings
+ scope :search, ->(query) { where "title LIKE ?", "%#{query}%" }
+
def hashtag
"#" + title
end
+
+ def to_combobox_display
+ title
+ end
end
diff --git a/app/models/user.rb b/app/models/user.rb
index d452c1756..ccd198dd9 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -23,6 +23,7 @@ class User < ApplicationRecord
scope :active, -> { where(active: true) }
scope :alphabetically, -> { order("LOWER(name)") }
+ scope :search, ->(query) { where "name LIKE ?", "%#{query}%" }
def initials
name.to_s.scan(/\b\p{L}/).join.upcase
@@ -40,6 +41,10 @@ class User < ApplicationRecord
other != self
end
+ def to_combobox_display
+ name
+ end
+
private
def deactived_email_address
email_address.sub(/@/, "-deactivated-#{SecureRandom.uuid}@")
diff --git a/app/views/bubbles/_assignments.html.erb b/app/views/bubbles/_assignments.html.erb
index eae3e7f3b..cb88d8fe3 100644
--- a/app/views/bubbles/_assignments.html.erb
+++ b/app/views/bubbles/_assignments.html.erb
@@ -1,14 +1,17 @@
-<% bubble.assignments.each do |assignment| %>
+<% bubble.assignees.each do |assignee| %>
- <%= form_with url: bucket_bubble_assignments_path(bubble.bucket, bubble), data: { controller: "form" } do |form| %>
-
- <% end %>
+
+
+ <%= form_with url: bucket_bubble_assignment_swaps_path(bubble.bucket, bubble), data: { controller: "form" } do |form| %>
+ <%= form.hidden_field :outgoing_assignee_id, value: assignee.id %>
+ <%= form.combobox :incoming_assignee_id, bucket_bubble_users_path(bubble.bucket, bubble),
+ id: dom_id(assignee, :assignment), required: true, preload: true,
+ data: { controller: "autofocus", action: "hw-combobox:selection->form#submit" } %>
+ <% end %>
+
<% end %>
diff --git a/app/views/bubbles/_tags.html.erb b/app/views/bubbles/_tags.html.erb
index be668f076..f7f2961b7 100644
--- a/app/views/bubbles/_tags.html.erb
+++ b/app/views/bubbles/_tags.html.erb
@@ -1,5 +1,16 @@
-<% @filter ||= Current.user.filters.last %>
-
<% bubble.tags.last(3).each do |tag| %>
- <%= link_to tag.hashtag, bubbles_path(@filter&.to_params&.merge(tag_ids: [ tag.id ])), class: "bubble__bubble bubble__meta bubble__tag" %>
+
+
+
+ <%= tag.hashtag %>
+
+
+ <%= form_with url: bucket_bubble_tagging_swaps_path(bubble.bucket, bubble), data: { controller: "form" } do |form| %>
+ <%= form.hidden_field :outgoing_tag_id, value: tag.id %>
+ <%= form.combobox :incoming_tag_id, bucket_bubble_tags_path(bubble.bucket, bubble),
+ id: dom_id(tag, :tagging), required: true, preload: true, name_when_new: "incoming_tag_title",
+ data: { controller: "autofocus", action: "hw-combobox:selection->form#submit" } %>
+ <% end %>
+
+
<% end %>
diff --git a/app/views/bubbles/show.html.erb b/app/views/bubbles/show.html.erb
index b3a863a7c..c2e4fdf04 100644
--- a/app/views/bubbles/show.html.erb
+++ b/app/views/bubbles/show.html.erb
@@ -25,44 +25,8 @@
<% end %>
- <%= form_with url: bucket_bubble_assignments_path(@bubble.bucket, @bubble), data: { controller: "form" } do |form| %>
-
- <% end %>
-
-
-
-
-
-
+ <%= render "bubbles/sidebar/assignment", bubble: @bubble %>
+ <%= render "bubbles/sidebar/tag", bubble: @bubble %>
<% if @bubble.image.attached? %>
<%= button_to bucket_bubble_image_path(@bubble.bucket, @bubble), method: :delete, class: "btn full-width justify-start borderless" do %>
diff --git a/app/views/bubbles/sidebar/_assignment.html.erb b/app/views/bubbles/sidebar/_assignment.html.erb
new file mode 100644
index 000000000..b7a43e164
--- /dev/null
+++ b/app/views/bubbles/sidebar/_assignment.html.erb
@@ -0,0 +1,11 @@
+
+
+ <%= image_tag "person.svg", aria: { hidden: true }, size: 24 %>
+ Assign to someone
+
+
+ <%= form_with url: bucket_bubble_assignment_toggles_path(bubble.bucket, bubble), data: { controller: "form" } do |form| %>
+ <%= form.combobox :assignee_id, bucket_bubble_users_path(bubble.bucket, bubble), required: true, preload: true,
+ data: { controller: "autofocus", action: "hw-combobox:selection->form#submit" } %>
+ <% end %>
+
diff --git a/app/views/bubbles/sidebar/_tag.html.erb b/app/views/bubbles/sidebar/_tag.html.erb
new file mode 100644
index 000000000..c8ae622b6
--- /dev/null
+++ b/app/views/bubbles/sidebar/_tag.html.erb
@@ -0,0 +1,12 @@
+
+
+ <%= image_tag "tag.svg", aria: { hidden: true }, size: 24 %>
+ Tag this
+
+
+ <%= form_with url: bucket_bubble_tagging_toggles_path(bubble.bucket, bubble), data: { controller: "form" } do |form| %>
+ <%= form.combobox :tag_id, bucket_bubble_tags_path(bubble.bucket, bubble),
+ required: true, preload: true, name_when_new: "tag_title",
+ data: { controller: "autofocus", action: "hw-combobox:selection->form#submit" } %>
+ <% end %>
+
diff --git a/app/views/bubbles/tags/_select_option.turbo_stream.erb b/app/views/bubbles/tags/_select_option.turbo_stream.erb
new file mode 100644
index 000000000..b1176ddc0
--- /dev/null
+++ b/app/views/bubbles/tags/_select_option.turbo_stream.erb
@@ -0,0 +1,7 @@
+
+ <%= tag.hashtag %>
+
+ <% if bubble.tagged_with?(tag) %>
+ <%= image_tag "check.svg", aria: { hidden: "true" } %>
+ <% end %>
+
diff --git a/app/views/bubbles/tags/index.turbo_stream.erb b/app/views/bubbles/tags/index.turbo_stream.erb
new file mode 100644
index 000000000..ea4cce4a5
--- /dev/null
+++ b/app/views/bubbles/tags/index.turbo_stream.erb
@@ -0,0 +1 @@
+<%= async_combobox_options @tags, render_in: { partial: "bubbles/tags/select_option", as: :tag, locals: { bubble: @bubble } } %>
diff --git a/app/views/bubbles/users/_select_option.turbo_stream.erb b/app/views/bubbles/users/_select_option.turbo_stream.erb
new file mode 100644
index 000000000..ec304a714
--- /dev/null
+++ b/app/views/bubbles/users/_select_option.turbo_stream.erb
@@ -0,0 +1,7 @@
+
+ <%= user.name %>
+
+ <% if bubble.assigned_to?(user) %>
+ <%= image_tag "check.svg", aria: { hidden: "true" } %>
+ <% end %>
+
diff --git a/app/views/bubbles/users/index.turbo_stream.erb b/app/views/bubbles/users/index.turbo_stream.erb
new file mode 100644
index 000000000..cf39ad084
--- /dev/null
+++ b/app/views/bubbles/users/index.turbo_stream.erb
@@ -0,0 +1 @@
+<%= async_combobox_options @users, render_in: { partial: "bubbles/users/select_option", as: :user, locals: { bubble: @bubble } } %>
diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb
index 0534b470b..d977d6af0 100644
--- a/app/views/layouts/application.html.erb
+++ b/app/views/layouts/application.html.erb
@@ -12,6 +12,7 @@
<%= csp_meta_tag %>
<%= tag.meta name: "current-user-id", content: Current.user.id if Current.user %>
+ <%= combobox_style_tag %>
<%= stylesheet_link_tag :app, "data-turbo-track": "reload" %>
<%= javascript_importmap_tags %>
diff --git a/config/initializers/hotwire_combobox.rb b/config/initializers/hotwire_combobox.rb
new file mode 100644
index 000000000..9735f6086
--- /dev/null
+++ b/config/initializers/hotwire_combobox.rb
@@ -0,0 +1,9 @@
+class ActionView::Helpers::FormBuilder
+ def combobox(...)
+ super do |combobox|
+ combobox.customize_main_wrapper class: "btn"
+ combobox.customize_input data: { autofocus_target: "element" }
+ yield combobox if block_given?
+ end
+ end
+end
diff --git a/config/routes.rb b/config/routes.rb
index 0778fb130..fd1d318ce 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -16,20 +16,28 @@ Rails.application.routes.draw do
resources :buckets do
resources :bubbles do
- resources :assignments
resources :boosts
resources :comments
- resources :tags, shallow: true
scope module: :bubbles do
resource :image
resource :pop
resource :stage_picker
resources :stagings
+ resources :tags
+ resources :users
+ end
+
+ namespace :assignments, as: :assignment do
+ resources :swaps
+ resources :toggles
+ end
+
+ namespace :taggings, as: :tagging do
+ resources :swaps
+ resources :toggles
end
end
-
- resources :tags, only: :index
end
resources :filters
diff --git a/db/migrate/20241120222444_make_taggings_unique_per_bubble.rb b/db/migrate/20241120222444_make_taggings_unique_per_bubble.rb
new file mode 100644
index 000000000..06edb6170
--- /dev/null
+++ b/db/migrate/20241120222444_make_taggings_unique_per_bubble.rb
@@ -0,0 +1,6 @@
+class MakeTaggingsUniquePerBubble < ActiveRecord::Migration[8.0]
+ def change
+ remove_index :taggings, :bubble_id
+ add_index :taggings, %i[ bubble_id tag_id ], unique: true
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 9754ac707..9895b7729 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_11_19_201943) do
+ActiveRecord::Schema[8.0].define(version: 2024_11_20_222444) do
create_table "accesses", force: :cascade do |t|
t.integer "bucket_id", null: false
t.integer "user_id", null: false
@@ -187,7 +187,7 @@ ActiveRecord::Schema[8.0].define(version: 2024_11_19_201943) do
t.integer "tag_id", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
- t.index ["bubble_id"], name: "index_taggings_on_bubble_id"
+ t.index ["bubble_id", "tag_id"], name: "index_taggings_on_bubble_id_and_tag_id", unique: true
t.index ["tag_id"], name: "index_taggings_on_tag_id"
end
diff --git a/test/controllers/assignments/swaps_controller_test.rb b/test/controllers/assignments/swaps_controller_test.rb
new file mode 100644
index 000000000..00c9b1a89
--- /dev/null
+++ b/test/controllers/assignments/swaps_controller_test.rb
@@ -0,0 +1,41 @@
+require "test_helper"
+
+class Assignments::SwapsControllerTest < ActionDispatch::IntegrationTest
+ setup do
+ sign_in_as :kevin
+ end
+
+ test "swap with same user" do
+ assert_changes "bubbles(:logo).assigned_to?(users(:kevin))", from: true, to: false do
+ post bucket_bubble_assignment_swaps_url(buckets(:writebook), bubbles(:logo)), params: {
+ incoming_assignee_id: users(:kevin).id,
+ outgoing_assignee_id: users(:kevin).id
+ }
+ end
+ assert_redirected_to bubbles(:logo)
+ end
+
+ test "swap with another user" do
+ assert_changes "bubbles(:logo).assigned_to?(users(:david))", from: false, to: true do
+ assert_changes "bubbles(:logo).assigned_to?(users(:kevin))", from: true, to: false do
+ post bucket_bubble_assignment_swaps_url(buckets(:writebook), bubbles(:logo)), params: {
+ incoming_assignee_id: users(:david).id,
+ outgoing_assignee_id: users(:kevin).id
+ }
+ end
+ end
+ assert_redirected_to bubbles(:logo)
+ end
+
+ test "swap with another user when already assigned" do
+ assert_no_changes "bubbles(:logo).assigned_to?(users(:jz))" do
+ assert_changes "bubbles(:logo).assigned_to?(users(:kevin))", from: true, to: false do
+ post bucket_bubble_assignment_swaps_url(buckets(:writebook), bubbles(:logo)), params: {
+ incoming_assignee_id: users(:jz).id,
+ outgoing_assignee_id: users(:kevin).id
+ }
+ end
+ end
+ assert_redirected_to bubbles(:logo)
+ end
+end
diff --git a/test/controllers/assignments/toggles_controller_test.rb b/test/controllers/assignments/toggles_controller_test.rb
new file mode 100644
index 000000000..b82e18099
--- /dev/null
+++ b/test/controllers/assignments/toggles_controller_test.rb
@@ -0,0 +1,19 @@
+require "test_helper"
+
+class Assignments::TogglesControllerTest < ActionDispatch::IntegrationTest
+ setup do
+ sign_in_as :kevin
+ end
+
+ test "create" do
+ assert_changes "bubbles(:logo).assigned_to?(users(:david))", from: false, to: true do
+ post bucket_bubble_assignment_toggles_url(buckets(:writebook), bubbles(:logo)), params: { assignee_id: users(:david).id }
+ end
+ assert_redirected_to bubbles(:logo)
+
+ assert_changes "bubbles(:logo).assigned_to?(users(:david))", from: true, to: false do
+ post bucket_bubble_assignment_toggles_url(buckets(:writebook), bubbles(:logo)), params: { assignee_id: users(:david).id }
+ end
+ assert_redirected_to bubbles(:logo)
+ end
+end
diff --git a/test/controllers/taggings/swaps_controller_test.rb b/test/controllers/taggings/swaps_controller_test.rb
new file mode 100644
index 000000000..0ebcee208
--- /dev/null
+++ b/test/controllers/taggings/swaps_controller_test.rb
@@ -0,0 +1,41 @@
+require "test_helper"
+
+class Taggings::SwapsControllerTest < ActionDispatch::IntegrationTest
+ setup do
+ sign_in_as :kevin
+ end
+
+ test "swap with same tag" do
+ assert_changes "bubbles(:logo).tagged_with?(tags(:web))", from: true, to: false do
+ post bucket_bubble_tagging_swaps_url(buckets(:writebook), bubbles(:logo)), params: {
+ incoming_tag_id: tags(:web).id,
+ outgoing_tag_id: tags(:web).id
+ }
+ end
+ assert_redirected_to bubbles(:logo)
+ end
+
+ test "swap with another tag" do
+ assert_changes "bubbles(:logo).tagged_with?(tags(:mobile))", from: false, to: true do
+ assert_changes "bubbles(:logo).tagged_with?(tags(:web))", from: true, to: false do
+ post bucket_bubble_tagging_swaps_url(buckets(:writebook), bubbles(:logo)), params: {
+ incoming_tag_id: tags(:mobile).id,
+ outgoing_tag_id: tags(:web).id
+ }
+ end
+ end
+ assert_redirected_to bubbles(:logo)
+ end
+
+ test "swap with another tag when already tagged" do
+ assert_no_changes "bubbles(:layout).tagged_with?(tags(:mobile))" do
+ assert_changes "bubbles(:layout).tagged_with?(tags(:web))", from: true, to: false do
+ post bucket_bubble_tagging_swaps_url(buckets(:writebook), bubbles(:layout)), params: {
+ incoming_tag_id: tags(:mobile).id,
+ outgoing_tag_id: tags(:web).id
+ }
+ end
+ end
+ assert_redirected_to bubbles(:layout)
+ end
+end
diff --git a/test/controllers/taggings/toggles_controller_test.rb b/test/controllers/taggings/toggles_controller_test.rb
new file mode 100644
index 000000000..fe9e76a8e
--- /dev/null
+++ b/test/controllers/taggings/toggles_controller_test.rb
@@ -0,0 +1,19 @@
+require "test_helper"
+
+class Taggings::TogglesControllerTest < ActionDispatch::IntegrationTest
+ setup do
+ sign_in_as :kevin
+ end
+
+ test "create" do
+ assert_changes "bubbles(:logo).tagged_with?(tags(:mobile))", from: false, to: true do
+ post bucket_bubble_tagging_toggles_url(buckets(:writebook), bubbles(:logo)), params: { tag_id: tags(:mobile).id }
+ end
+ assert_redirected_to bubbles(:logo)
+
+ assert_changes "bubbles(:logo).tagged_with?(tags(:mobile))", from: true, to: false do
+ post bucket_bubble_tagging_toggles_url(buckets(:writebook), bubbles(:logo)), params: { tag_id: tags(:mobile).id }
+ end
+ assert_redirected_to bubbles(:logo)
+ end
+end
diff --git a/test/controllers/tags_controller_test.rb b/test/controllers/tags_controller_test.rb
deleted file mode 100644
index 909b806e2..000000000
--- a/test/controllers/tags_controller_test.rb
+++ /dev/null
@@ -1,24 +0,0 @@
-require "test_helper"
-
-class TagsControllerTest < ActionDispatch::IntegrationTest
- setup do
- sign_in_as :kevin
- end
-
- test "create with existing tag" do
- assert_no_difference -> { accounts(:"37s").tags.count } do
- post bucket_bubble_tags_url(buckets(:writebook), bubbles(:logo)), params: { tag: { title: "Web" } }
- end
-
- assert_redirected_to bubbles(:logo)
- end
-
- test "create with new tag" do
- assert_difference -> { accounts(:"37s").tags.count }, +1 do
- post bucket_bubble_tags_url(buckets(:writebook), bubbles(:logo)), params: { tag: { title: "Horizons" } }
- end
-
- assert_redirected_to bubbles(:logo)
- assert bubbles(:logo).tags.pluck(:title).include?("Horizons")
- end
-end