From f266e46b57d73c52532807eb91a459cb494a8e71 Mon Sep 17 00:00:00 2001 From: Jose Farias Date: Thu, 21 Nov 2024 19:58:21 -0600 Subject: [PATCH 01/11] Wire up comboboxes --- Gemfile | 1 + Gemfile.lock | 15 +++++++ app/assets/stylesheets/bubbles.css | 4 ++ app/assets/stylesheets/comboboxes.css | 33 +++++++++++++++ .../assignments/swaps_controller.rb | 17 ++++++++ .../assignments/toggles_controller.rb | 13 ++++++ app/controllers/assignments_controller.rb | 19 --------- app/controllers/bubbles/tags_controller.rb | 7 ++++ app/controllers/bubbles/users_controller.rb | 7 ++++ app/controllers/taggings/swaps_controller.rb | 21 ++++++++++ .../taggings/toggles_controller.rb | 17 ++++++++ app/controllers/tags_controller.rb | 23 ----------- .../controllers/autofocus_controller.js | 16 ++++++++ .../controllers/expandable_controller.js | 9 ++++ app/models/assignment.rb | 2 - app/models/bubble/assignable.rb | 26 ++++++++++-- app/models/bubble/taggable.rb | 25 ++++++++++- app/models/event_summary.rb | 2 + app/models/tag.rb | 8 +++- app/models/user.rb | 5 +++ app/views/bubbles/_assignments.html.erb | 23 ++++++----- app/views/bubbles/_tags.html.erb | 17 ++++++-- app/views/bubbles/show.html.erb | 40 +----------------- .../bubbles/sidebar/_assignment.html.erb | 11 +++++ app/views/bubbles/sidebar/_tag.html.erb | 12 ++++++ .../tags/_select_option.turbo_stream.erb | 7 ++++ app/views/bubbles/tags/index.turbo_stream.erb | 1 + .../users/_select_option.turbo_stream.erb | 7 ++++ .../bubbles/users/index.turbo_stream.erb | 1 + app/views/layouts/application.html.erb | 1 + config/initializers/hotwire_combobox.rb | 9 ++++ config/routes.rb | 16 ++++++-- ...0222444_make_taggings_unique_per_bubble.rb | 6 +++ db/schema.rb | 4 +- .../assignments/swaps_controller_test.rb | 41 +++++++++++++++++++ .../assignments/toggles_controller_test.rb | 19 +++++++++ .../taggings/swaps_controller_test.rb | 41 +++++++++++++++++++ .../taggings/toggles_controller_test.rb | 19 +++++++++ test/controllers/tags_controller_test.rb | 24 ----------- 39 files changed, 437 insertions(+), 132 deletions(-) create mode 100644 app/assets/stylesheets/comboboxes.css create mode 100644 app/controllers/assignments/swaps_controller.rb create mode 100644 app/controllers/assignments/toggles_controller.rb delete mode 100644 app/controllers/assignments_controller.rb create mode 100644 app/controllers/bubbles/tags_controller.rb create mode 100644 app/controllers/bubbles/users_controller.rb create mode 100644 app/controllers/taggings/swaps_controller.rb create mode 100644 app/controllers/taggings/toggles_controller.rb delete mode 100644 app/controllers/tags_controller.rb create mode 100644 app/javascript/controllers/autofocus_controller.js create mode 100644 app/javascript/controllers/expandable_controller.js create mode 100644 app/views/bubbles/sidebar/_assignment.html.erb create mode 100644 app/views/bubbles/sidebar/_tag.html.erb create mode 100644 app/views/bubbles/tags/_select_option.turbo_stream.erb create mode 100644 app/views/bubbles/tags/index.turbo_stream.erb create mode 100644 app/views/bubbles/users/_select_option.turbo_stream.erb create mode 100644 app/views/bubbles/users/index.turbo_stream.erb create mode 100644 config/initializers/hotwire_combobox.rb create mode 100644 db/migrate/20241120222444_make_taggings_unique_per_bubble.rb create mode 100644 test/controllers/assignments/swaps_controller_test.rb create mode 100644 test/controllers/assignments/toggles_controller_test.rb create mode 100644 test/controllers/taggings/swaps_controller_test.rb create mode 100644 test/controllers/taggings/toggles_controller_test.rb delete mode 100644 test/controllers/tags_controller_test.rb 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 %> 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 %> - -
- - - - <%= image_tag "tag.svg", aria: { hidden: true }, size: 24 %> - - <%= form_with model: Tag.new, url: bucket_bubble_tags_path(@bubble.bucket, @bubble), class: "min-width flex-item-grow", data: { turbo_frame: "_top" } do |form| %> - <%= form.text_field :title, class: "input", autofocus: "on", list: "tags-list" %> - <%= form.submit "Create Tag", hidden: true %> - - - <%= Current.account.tags.each do |tag| %> - - <% end %> - - <% 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 From 505aab6b573e7ee51e885fab8c5e2b41b0f455ba Mon Sep 17 00:00:00 2001 From: Jose Farias Date: Mon, 25 Nov 2024 16:15:31 -0600 Subject: [PATCH 02/11] Fix assignment styles in bubble --- app/assets/stylesheets/bubbles.css | 5 ----- app/views/bubbles/_assignments.html.erb | 1 + 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/app/assets/stylesheets/bubbles.css b/app/assets/stylesheets/bubbles.css index 46208c8d3..8bd49c974 100644 --- a/app/assets/stylesheets/bubbles.css +++ b/app/assets/stylesheets/bubbles.css @@ -134,11 +134,6 @@ z-index: 2; } - select { - inset: 0; - position: absolute; - } - .avatar { border-radius: var(--bubble-shape); } diff --git a/app/views/bubbles/_assignments.html.erb b/app/views/bubbles/_assignments.html.erb index b66972a63..7206ef4f5 100644 --- a/app/views/bubbles/_assignments.html.erb +++ b/app/views/bubbles/_assignments.html.erb @@ -3,6 +3,7 @@
<%= assignee.name %> <%= avatar_image_tag assignee, loading: :lazy, class: "avatar flex-item-no-shrink" %> + Assigned to <%= assignee.name %>
<% end %> From 5e09f5a9b9ae1a976c8462535393b2948dd9bdb3 Mon Sep 17 00:00:00 2001 From: Jose Farias Date: Mon, 25 Nov 2024 16:17:44 -0600 Subject: [PATCH 03/11] Remove swapping --- app/assets/stylesheets/bubbles.css | 4 -- app/assets/stylesheets/comboboxes.css | 13 ------ .../assignments/swaps_controller.rb | 17 -------- app/controllers/taggings/swaps_controller.rb | 21 ---------- app/models/bubble/assignable.rb | 7 ---- app/models/bubble/taggable.rb | 7 ---- config/routes.rb | 2 - .../assignments/swaps_controller_test.rb | 41 ------------------- .../taggings/swaps_controller_test.rb | 41 ------------------- 9 files changed, 153 deletions(-) delete mode 100644 app/controllers/assignments/swaps_controller.rb delete mode 100644 app/controllers/taggings/swaps_controller.rb delete mode 100644 test/controllers/assignments/swaps_controller_test.rb delete mode 100644 test/controllers/taggings/swaps_controller_test.rb diff --git a/app/assets/stylesheets/bubbles.css b/app/assets/stylesheets/bubbles.css index 8bd49c974..e327e8150 100644 --- a/app/assets/stylesheets/bubbles.css +++ b/app/assets/stylesheets/bubbles.css @@ -130,10 +130,6 @@ } } - &:focus-within { - z-index: 2; - } - .avatar { border-radius: var(--bubble-shape); } diff --git a/app/assets/stylesheets/comboboxes.css b/app/assets/stylesheets/comboboxes.css index 3cfce33c9..5c474ea59 100644 --- a/app/assets/stylesheets/comboboxes.css +++ b/app/assets/stylesheets/comboboxes.css @@ -18,16 +18,3 @@ 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 deleted file mode 100644 index 3554a1369..000000000 --- a/app/controllers/assignments/swaps_controller.rb +++ /dev/null @@ -1,17 +0,0 @@ -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/taggings/swaps_controller.rb b/app/controllers/taggings/swaps_controller.rb deleted file mode 100644 index b335c1a6c..000000000 --- a/app/controllers/taggings/swaps_controller.rb +++ /dev/null @@ -1,21 +0,0 @@ -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/models/bubble/assignable.rb b/app/models/bubble/assignable.rb index 5bcd241fd..128d76e5b 100644 --- a/app/models/bubble/assignable.rb +++ b/app/models/bubble/assignable.rb @@ -22,13 +22,6 @@ module Bubble::Assignable track_event :unassigned, assignee_ids: [ user.id ] if destructions.any? end - def swap_assignment(incoming, outgoing) - transaction do - unassign outgoing - assign incoming unless incoming == outgoing - end - end - def toggle_assignment(user) assigned_to?(user) ? unassign(user) : assign(user) end diff --git a/app/models/bubble/taggable.rb b/app/models/bubble/taggable.rb index d32e58a27..0ab168fce 100644 --- a/app/models/bubble/taggable.rb +++ b/app/models/bubble/taggable.rb @@ -18,13 +18,6 @@ module Bubble::Taggable 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 diff --git a/config/routes.rb b/config/routes.rb index fd1d318ce..ebddd9256 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -29,12 +29,10 @@ Rails.application.routes.draw do end namespace :assignments, as: :assignment do - resources :swaps resources :toggles end namespace :taggings, as: :tagging do - resources :swaps resources :toggles end end diff --git a/test/controllers/assignments/swaps_controller_test.rb b/test/controllers/assignments/swaps_controller_test.rb deleted file mode 100644 index 00c9b1a89..000000000 --- a/test/controllers/assignments/swaps_controller_test.rb +++ /dev/null @@ -1,41 +0,0 @@ -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/taggings/swaps_controller_test.rb b/test/controllers/taggings/swaps_controller_test.rb deleted file mode 100644 index 0ebcee208..000000000 --- a/test/controllers/taggings/swaps_controller_test.rb +++ /dev/null @@ -1,41 +0,0 @@ -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 From acac683b3e462fd7146e5f864b016c09cac8df08 Mon Sep 17 00:00:00 2001 From: Jose Farias Date: Mon, 25 Nov 2024 16:25:42 -0600 Subject: [PATCH 04/11] Do combobox filtering client-side --- app/controllers/bubbles/tags_controller.rb | 7 --- app/controllers/bubbles/users_controller.rb | 7 --- app/models/tag.rb | 2 - app/models/user.rb | 1 - app/views/bubbles/show.html.erb | 43 +------------------ .../bubbles/sidebar/_assignment.html.erb | 3 +- app/views/bubbles/sidebar/_tag.html.erb | 4 +- ...rbo_stream.erb => _select_option.html.erb} | 0 app/views/bubbles/tags/index.turbo_stream.erb | 1 - ...rbo_stream.erb => _select_option.html.erb} | 0 .../bubbles/users/index.turbo_stream.erb | 1 - config/routes.rb | 2 - 12 files changed, 6 insertions(+), 65 deletions(-) delete mode 100644 app/controllers/bubbles/tags_controller.rb delete mode 100644 app/controllers/bubbles/users_controller.rb rename app/views/bubbles/tags/{_select_option.turbo_stream.erb => _select_option.html.erb} (100%) delete mode 100644 app/views/bubbles/tags/index.turbo_stream.erb rename app/views/bubbles/users/{_select_option.turbo_stream.erb => _select_option.html.erb} (100%) delete mode 100644 app/views/bubbles/users/index.turbo_stream.erb diff --git a/app/controllers/bubbles/tags_controller.rb b/app/controllers/bubbles/tags_controller.rb deleted file mode 100644 index 4ac791ae0..000000000 --- a/app/controllers/bubbles/tags_controller.rb +++ /dev/null @@ -1,7 +0,0 @@ -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 deleted file mode 100644 index 23b8b9009..000000000 --- a/app/controllers/bubbles/users_controller.rb +++ /dev/null @@ -1,7 +0,0 @@ -class Bubbles::UsersController < ApplicationController - include BubbleScoped, BucketScoped - - def index - @users = @bucket.users.active.search params[:q] - end -end diff --git a/app/models/tag.rb b/app/models/tag.rb index 5937ea8cd..e11391e0b 100644 --- a/app/models/tag.rb +++ b/app/models/tag.rb @@ -6,8 +6,6 @@ class Tag < ApplicationRecord has_many :taggings, dependent: :destroy has_many :bubbles, through: :taggings - scope :search, ->(query) { where "title LIKE ?", "%#{query}%" } - def hashtag "#" + title end diff --git a/app/models/user.rb b/app/models/user.rb index ccd198dd9..2baa83876 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -23,7 +23,6 @@ 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 diff --git a/app/views/bubbles/show.html.erb b/app/views/bubbles/show.html.erb index f6475cdcc..959f5093b 100644 --- a/app/views/bubbles/show.html.erb +++ b/app/views/bubbles/show.html.erb @@ -23,47 +23,8 @@ <% end %> - <%= render "bubbles/sidebar/assignment", bubble: @bubble %> - <%= render "bubbles/sidebar/tag", bubble: @bubble %> - - <%= form_with url: "/", data: { controller: "form" } do |form| %> - - <% end %> - -
- - - - <%= image_tag "tag.svg", aria: { hidden: true }, size: 24 %> - - <%= form_with model: Tag.new, url: bucket_bubble_tags_path(@bubble.bucket, @bubble), class: "min-width flex-item-grow", data: { turbo_frame: "_top" } do |form| %> - <%= form.text_field :title, class: "input", autofocus: "on", list: "tags-list" %> - <%= form.submit "Create Tag", hidden: true %> - - - <%= Current.account.tags.each do |tag| %> - - <% end %> - - <% end %> - -
- -
-
-
+ <%= render "bubbles/sidebar/assignment", bubble: @bubble, users: @bucket.users.active %> + <%= render "bubbles/sidebar/tag", bubble: @bubble, tags: Current.account.tags %> <% 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 index b7a43e164..29fad3aa6 100644 --- a/app/views/bubbles/sidebar/_assignment.html.erb +++ b/app/views/bubbles/sidebar/_assignment.html.erb @@ -5,7 +5,8 @@ <%= 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, + <%= form.combobox :assignee_id, users, required: true, preload: true, + render_in: { partial: "bubbles/users/select_option", as: :user, locals: { bubble: bubble } }, 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 index c8ae622b6..a203790ab 100644 --- a/app/views/bubbles/sidebar/_tag.html.erb +++ b/app/views/bubbles/sidebar/_tag.html.erb @@ -5,8 +5,8 @@ <%= 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", + <%= form.combobox :tag_id, tags, required: true, preload: true, name_when_new: "tag_title", + render_in: { partial: "bubbles/tags/select_option", as: :tag, locals: { bubble: bubble } }, 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.html.erb similarity index 100% rename from app/views/bubbles/tags/_select_option.turbo_stream.erb rename to app/views/bubbles/tags/_select_option.html.erb diff --git a/app/views/bubbles/tags/index.turbo_stream.erb b/app/views/bubbles/tags/index.turbo_stream.erb deleted file mode 100644 index ea4cce4a5..000000000 --- a/app/views/bubbles/tags/index.turbo_stream.erb +++ /dev/null @@ -1 +0,0 @@ -<%= 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.html.erb similarity index 100% rename from app/views/bubbles/users/_select_option.turbo_stream.erb rename to app/views/bubbles/users/_select_option.html.erb diff --git a/app/views/bubbles/users/index.turbo_stream.erb b/app/views/bubbles/users/index.turbo_stream.erb deleted file mode 100644 index cf39ad084..000000000 --- a/app/views/bubbles/users/index.turbo_stream.erb +++ /dev/null @@ -1 +0,0 @@ -<%= async_combobox_options @users, render_in: { partial: "bubbles/users/select_option", as: :user, locals: { bubble: @bubble } } %> diff --git a/config/routes.rb b/config/routes.rb index ebddd9256..c11aff6ee 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -24,8 +24,6 @@ Rails.application.routes.draw do resource :pop resource :stage_picker resources :stagings - resources :tags - resources :users end namespace :assignments, as: :assignment do From 43add9d0bec91d5117615b77e9101ce07e61c6d3 Mon Sep 17 00:00:00 2001 From: Jose Farias Date: Mon, 25 Nov 2024 16:30:38 -0600 Subject: [PATCH 05/11] Move combobox initializer to helper --- .../hotwire_combobox.rb => app/helpers/combobox_helper.rb | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) rename config/initializers/hotwire_combobox.rb => app/helpers/combobox_helper.rb (59%) diff --git a/config/initializers/hotwire_combobox.rb b/app/helpers/combobox_helper.rb similarity index 59% rename from config/initializers/hotwire_combobox.rb rename to app/helpers/combobox_helper.rb index 9735f6086..c9bb10c10 100644 --- a/config/initializers/hotwire_combobox.rb +++ b/app/helpers/combobox_helper.rb @@ -1,5 +1,6 @@ -class ActionView::Helpers::FormBuilder - def combobox(...) +module ComboboxHelper + # `hw_combobox_tag` overrides HotwireCombobox's helper to provide default options. + def hw_combobox_tag(...) super do |combobox| combobox.customize_main_wrapper class: "btn" combobox.customize_input data: { autofocus_target: "element" } From 36d930cd0e491e1a0f4ebe6f75e614dff382c684 Mon Sep 17 00:00:00 2001 From: Jose Farias Date: Mon, 25 Nov 2024 17:49:28 -0600 Subject: [PATCH 06/11] Add tests for tagging and assigning --- test/models/bubble_test.rb | 74 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 71 insertions(+), 3 deletions(-) diff --git a/test/models/bubble_test.rb b/test/models/bubble_test.rb index 40cdbe4ad..f5d730769 100644 --- a/test/models/bubble_test.rb +++ b/test/models/bubble_test.rb @@ -19,13 +19,81 @@ class BubbleTest < ActiveSupport::TestCase end end - test "assigning" do - bubbles(:logo).assign users(:david) + test "assignment" do + assert bubbles(:logo).assigned_to?(users(:kevin)) + assert_not bubbles(:logo).assigned_to?(users(:david)) + end - assert_equal users(:kevin, :jz, :david), bubbles(:logo).assignees + test "assigning" do + assert_difference %w[ bubbles(:logo).assignees.count Event.count ], +1 do + bubbles(:logo).assign users(:david) + bubbles(:logo).assign users(:david) # idempotent + end + + assert bubbles(:logo).assigned_to?(users(:david)) + assert_equal "assigned", Event.last.action assert_equal [ users(:david) ], Event.last.assignees end + test "unassigning" do + assert_difference({ "bubbles(:logo).assignees.count" => -1, "Event.count" => +1 }) do + bubbles(:logo).unassign users(:kevin) + bubbles(:logo).unassign users(:kevin) # idempotent + end + + assert_not bubbles(:logo).assigned_to?(users(:kevin)) + assert_equal "unassigned", Event.last.action + assert_equal [ users(:kevin) ], Event.last.assignees + end + + test "toggling assignment" do + assert bubbles(:logo).assigned_to?(users(:kevin)) + + bubbles(:logo).toggle_assignment users(:kevin) + assert_not bubbles(:logo).assigned_to?(users(:kevin)) + + bubbles(:logo).toggle_assignment users(:kevin) + assert bubbles(:logo).assigned_to?(users(:kevin)) + end + + test "tag state" do + assert bubbles(:logo).tagged_with?(tags(:web)) + assert_not bubbles(:logo).tagged_with?(tags(:mobile)) + end + + test "tagging" do + assert_difference "bubbles(:logo).taggings.count", +1 do + bubbles(:logo).tag tags(:mobile) + bubbles(:logo).tag tags(:mobile) # idempotent + end + + assert bubbles(:logo).tagged_with?(tags(:mobile)) + + assert_difference %w[ bubbles(:logo).taggings.count accounts("37s").tags.count ], +1 do + bubbles(:logo).tag Tag.new(title: "prioritized") + end + + assert_equal "prioritized", bubbles(:logo).taggings.last.tag.title + end + + test "untagging" do + assert_difference "bubbles(:logo).taggings.count", -1 do + bubbles(:logo).untag tags(:web) + end + + assert_not bubbles(:logo).tagged_with?(tags(:web)) + end + + test "toggling tag" do + assert bubbles(:logo).tagged_with?(tags(:web)) + + bubbles(:logo).toggle_tag tags(:web) + assert_not bubbles(:logo).tagged_with?(tags(:web)) + + bubbles(:logo).toggle_tag tags(:web) + assert bubbles(:logo).tagged_with?(tags(:web)) + end + test "searchable by title" do bubble = buckets(:writebook).bubbles.create! title: "Insufficient haggis", creator: users(:kevin) From a9f845ed47c982a5b6503ff2c02a6f93c3216249 Mon Sep 17 00:00:00 2001 From: Jose Farias Date: Mon, 25 Nov 2024 17:51:20 -0600 Subject: [PATCH 07/11] Assigner is always Current.user --- app/models/bubble/assignable.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/models/bubble/assignable.rb b/app/models/bubble/assignable.rb index 128d76e5b..c91e52a39 100644 --- a/app/models/bubble/assignable.rb +++ b/app/models/bubble/assignable.rb @@ -10,8 +10,8 @@ module Bubble::Assignable scope :assigned_by, ->(users) { joins(:assignments).where(assignments: { assigner: users }).distinct } end - def assign(user, assigner: Current.user) - assignments.create! assignee: user, assigner: assigner + def assign(user) + assignments.create! assignee: user, assigner: Current.user track_event :assigned, assignee_ids: [ user.id ] rescue ActiveRecord::RecordNotUnique # Already assigned From 3e1b2c74e4f7dc6c97711e289294506e44aa9594 Mon Sep 17 00:00:00 2001 From: Jose Farias Date: Mon, 25 Nov 2024 17:55:25 -0600 Subject: [PATCH 08/11] Make one-way assignment and tagging methods private --- app/models/bubble/assignable.rb | 25 +++++------ app/models/bubble/taggable.rb | 21 +++++----- test/models/bubble_test.rb | 74 ++++++++++----------------------- 3 files changed, 47 insertions(+), 73 deletions(-) diff --git a/app/models/bubble/assignable.rb b/app/models/bubble/assignable.rb index c91e52a39..1e6d0bf56 100644 --- a/app/models/bubble/assignable.rb +++ b/app/models/bubble/assignable.rb @@ -10,18 +10,6 @@ module Bubble::Assignable scope :assigned_by, ->(users) { joins(:assignments).where(assignments: { assigner: users }).distinct } end - def assign(user) - assignments.create! assignee: user, assigner: Current.user - 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 toggle_assignment(user) assigned_to?(user) ? unassign(user) : assign(user) end @@ -29,4 +17,17 @@ module Bubble::Assignable def assigned_to?(user) assignments.exists? assignee: user end + + private + def assign(user) + assignments.create! assignee: user, assigner: Current.user + 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 end diff --git a/app/models/bubble/taggable.rb b/app/models/bubble/taggable.rb index 0ab168fce..6b6462e9b 100644 --- a/app/models/bubble/taggable.rb +++ b/app/models/bubble/taggable.rb @@ -8,16 +8,6 @@ module Bubble::Taggable scope :tagged_with, ->(tags) { joins(:taggings).where(taggings: { tag: tags }) } end - def tag(tag) - taggings.create! tag: tag - rescue ActiveRecord::RecordNotUnique - # Already tagged - end - - def untag(tag) - taggings.destroy_by tag: tag - end - def toggle_tag(tag) tagged_with?(tag) ? untag(tag) : tag(tag) end @@ -25,4 +15,15 @@ module Bubble::Taggable def tagged_with?(tag) tags.include? tag end + + private + def tag(tag) + taggings.create! tag: tag + rescue ActiveRecord::RecordNotUnique + # Already tagged + end + + def untag(tag) + taggings.destroy_by tag: tag + end end diff --git a/test/models/bubble_test.rb b/test/models/bubble_test.rb index f5d730769..d067be864 100644 --- a/test/models/bubble_test.rb +++ b/test/models/bubble_test.rb @@ -24,76 +24,48 @@ class BubbleTest < ActiveSupport::TestCase assert_not bubbles(:logo).assigned_to?(users(:david)) end - test "assigning" do - assert_difference %w[ bubbles(:logo).assignees.count Event.count ], +1 do - bubbles(:logo).assign users(:david) - bubbles(:logo).assign users(:david) # idempotent - end - - assert bubbles(:logo).assigned_to?(users(:david)) - assert_equal "assigned", Event.last.action - assert_equal [ users(:david) ], Event.last.assignees - end - - test "unassigning" do - assert_difference({ "bubbles(:logo).assignees.count" => -1, "Event.count" => +1 }) do - bubbles(:logo).unassign users(:kevin) - bubbles(:logo).unassign users(:kevin) # idempotent - end - - assert_not bubbles(:logo).assigned_to?(users(:kevin)) - assert_equal "unassigned", Event.last.action - assert_equal [ users(:kevin) ], Event.last.assignees - end - test "toggling assignment" do assert bubbles(:logo).assigned_to?(users(:kevin)) - bubbles(:logo).toggle_assignment users(:kevin) + assert_difference({ "bubbles(:logo).assignees.count" => -1, "Event.count" => +1 }) do + bubbles(:logo).toggle_assignment users(:kevin) + end assert_not bubbles(:logo).assigned_to?(users(:kevin)) + assert_equal "unassigned", Event.last.action + assert_equal [ users(:kevin) ], Event.last.assignees - bubbles(:logo).toggle_assignment users(:kevin) + assert_difference %w[ bubbles(:logo).assignees.count Event.count ], +1 do + bubbles(:logo).toggle_assignment users(:kevin) + end assert bubbles(:logo).assigned_to?(users(:kevin)) + assert_equal "assigned", Event.last.action + assert_equal [ users(:kevin) ], Event.last.assignees end - test "tag state" do + test "tagging" do assert bubbles(:logo).tagged_with?(tags(:web)) assert_not bubbles(:logo).tagged_with?(tags(:mobile)) end - test "tagging" do - assert_difference "bubbles(:logo).taggings.count", +1 do - bubbles(:logo).tag tags(:mobile) - bubbles(:logo).tag tags(:mobile) # idempotent - end + test "toggling tags" do + assert bubbles(:logo).tagged_with?(tags(:web)) - assert bubbles(:logo).tagged_with?(tags(:mobile)) + assert_difference "bubbles(:logo).taggings.count", -1 do + bubbles(:logo).toggle_tag tags(:web) + end + assert_not bubbles(:logo).tagged_with?(tags(:web)) + + assert_difference "bubbles(:logo).taggings.count", +1 do + bubbles(:logo).toggle_tag tags(:web) + end + assert bubbles(:logo).tagged_with?(tags(:web)) assert_difference %w[ bubbles(:logo).taggings.count accounts("37s").tags.count ], +1 do - bubbles(:logo).tag Tag.new(title: "prioritized") + bubbles(:logo).toggle_tag Tag.new(title: "prioritized") end - assert_equal "prioritized", bubbles(:logo).taggings.last.tag.title end - test "untagging" do - assert_difference "bubbles(:logo).taggings.count", -1 do - bubbles(:logo).untag tags(:web) - end - - assert_not bubbles(:logo).tagged_with?(tags(:web)) - end - - test "toggling tag" do - assert bubbles(:logo).tagged_with?(tags(:web)) - - bubbles(:logo).toggle_tag tags(:web) - assert_not bubbles(:logo).tagged_with?(tags(:web)) - - bubbles(:logo).toggle_tag tags(:web) - assert bubbles(:logo).tagged_with?(tags(:web)) - end - test "searchable by title" do bubble = buckets(:writebook).bubbles.create! title: "Insufficient haggis", creator: users(:kevin) From f53cf49e5a1765c126d73c1131a39b73ac37f81e Mon Sep 17 00:00:00 2001 From: Jose Farias Date: Mon, 25 Nov 2024 18:28:46 -0600 Subject: [PATCH 09/11] select_option -> listbox_option --- app/views/bubbles/sidebar/_assignment.html.erb | 2 +- app/views/bubbles/sidebar/_tag.html.erb | 2 +- .../tags/{_select_option.html.erb => _listbox_option.html.erb} | 0 .../users/{_select_option.html.erb => _listbox_option.html.erb} | 0 4 files changed, 2 insertions(+), 2 deletions(-) rename app/views/bubbles/tags/{_select_option.html.erb => _listbox_option.html.erb} (100%) rename app/views/bubbles/users/{_select_option.html.erb => _listbox_option.html.erb} (100%) diff --git a/app/views/bubbles/sidebar/_assignment.html.erb b/app/views/bubbles/sidebar/_assignment.html.erb index 29fad3aa6..e830ba64a 100644 --- a/app/views/bubbles/sidebar/_assignment.html.erb +++ b/app/views/bubbles/sidebar/_assignment.html.erb @@ -6,7 +6,7 @@ <%= form_with url: bucket_bubble_assignment_toggles_path(bubble.bucket, bubble), data: { controller: "form" } do |form| %> <%= form.combobox :assignee_id, users, required: true, preload: true, - render_in: { partial: "bubbles/users/select_option", as: :user, locals: { bubble: bubble } }, + render_in: { partial: "bubbles/users/listbox_option", as: :user, locals: { bubble: bubble } }, 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 index a203790ab..853a1f642 100644 --- a/app/views/bubbles/sidebar/_tag.html.erb +++ b/app/views/bubbles/sidebar/_tag.html.erb @@ -6,7 +6,7 @@ <%= form_with url: bucket_bubble_tagging_toggles_path(bubble.bucket, bubble), data: { controller: "form" } do |form| %> <%= form.combobox :tag_id, tags, required: true, preload: true, name_when_new: "tag_title", - render_in: { partial: "bubbles/tags/select_option", as: :tag, locals: { bubble: bubble } }, + render_in: { partial: "bubbles/tags/listbox_option", as: :tag, locals: { bubble: bubble } }, data: { controller: "autofocus", action: "hw-combobox:selection->form#submit" } %> <% end %> diff --git a/app/views/bubbles/tags/_select_option.html.erb b/app/views/bubbles/tags/_listbox_option.html.erb similarity index 100% rename from app/views/bubbles/tags/_select_option.html.erb rename to app/views/bubbles/tags/_listbox_option.html.erb diff --git a/app/views/bubbles/users/_select_option.html.erb b/app/views/bubbles/users/_listbox_option.html.erb similarity index 100% rename from app/views/bubbles/users/_select_option.html.erb rename to app/views/bubbles/users/_listbox_option.html.erb From cc891a2ad6378882595b425165a32f6017b7243c Mon Sep 17 00:00:00 2001 From: Jose Farias Date: Mon, 25 Nov 2024 20:34:04 -0600 Subject: [PATCH 10/11] No need to preload when not async --- app/views/bubbles/sidebar/_assignment.html.erb | 2 +- app/views/bubbles/sidebar/_tag.html.erb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/bubbles/sidebar/_assignment.html.erb b/app/views/bubbles/sidebar/_assignment.html.erb index e830ba64a..2236cb639 100644 --- a/app/views/bubbles/sidebar/_assignment.html.erb +++ b/app/views/bubbles/sidebar/_assignment.html.erb @@ -5,7 +5,7 @@ <%= form_with url: bucket_bubble_assignment_toggles_path(bubble.bucket, bubble), data: { controller: "form" } do |form| %> - <%= form.combobox :assignee_id, users, required: true, preload: true, + <%= form.combobox :assignee_id, users, required: true, render_in: { partial: "bubbles/users/listbox_option", as: :user, locals: { bubble: bubble } }, 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 index 853a1f642..c367be378 100644 --- a/app/views/bubbles/sidebar/_tag.html.erb +++ b/app/views/bubbles/sidebar/_tag.html.erb @@ -5,7 +5,7 @@ <%= form_with url: bucket_bubble_tagging_toggles_path(bubble.bucket, bubble), data: { controller: "form" } do |form| %> - <%= form.combobox :tag_id, tags, required: true, preload: true, name_when_new: "tag_title", + <%= form.combobox :tag_id, tags, required: true, name_when_new: "tag_title", render_in: { partial: "bubbles/tags/listbox_option", as: :tag, locals: { bubble: bubble } }, data: { controller: "autofocus", action: "hw-combobox:selection->form#submit" } %> <% end %> From 11e9e691bf611625e8d9c716b529cfcf6c327ec7 Mon Sep 17 00:00:00 2001 From: Jose Farias Date: Mon, 25 Nov 2024 20:35:53 -0600 Subject: [PATCH 11/11] Clean up test names --- test/models/bubble_test.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/models/bubble_test.rb b/test/models/bubble_test.rb index d067be864..d32ec380c 100644 --- a/test/models/bubble_test.rb +++ b/test/models/bubble_test.rb @@ -19,12 +19,12 @@ class BubbleTest < ActiveSupport::TestCase end end - test "assignment" do + test "assignment states" do assert bubbles(:logo).assigned_to?(users(:kevin)) assert_not bubbles(:logo).assigned_to?(users(:david)) end - test "toggling assignment" do + test "assignment toggling" do assert bubbles(:logo).assigned_to?(users(:kevin)) assert_difference({ "bubbles(:logo).assignees.count" => -1, "Event.count" => +1 }) do @@ -42,12 +42,12 @@ class BubbleTest < ActiveSupport::TestCase assert_equal [ users(:kevin) ], Event.last.assignees end - test "tagging" do + test "tagged states" do assert bubbles(:logo).tagged_with?(tags(:web)) assert_not bubbles(:logo).tagged_with?(tags(:mobile)) end - test "toggling tags" do + test "tag toggling" do assert bubbles(:logo).tagged_with?(tags(:web)) assert_difference "bubbles(:logo).taggings.count", -1 do