Merge branch 'main' into house

* main: (23 commits)
  Improve the flow for editing bubble titles
  Use path helpers in tests
  Revising access shouldn't do a replace turbo-action
  Split into new tests
  Update this test, too
  Adjust tests to match new behavior
  Reapply "Destroy equivalent filters upon resource removal"
  Revert "Destroy equivalent filters upon resource removal"
  Destroy equivalent filters upon resource removal
  Don't autocomplete
  Rework boosts form so you can enter any integer
  Pull out access_menu_tag
  Punt on removing filters for inaccessible buckets
  Can't see bubbles in buckets you've lost access to
  Users can remove themselves from buckets
  Fix redirect assertion
  Fix redirect assertion
  Submit form when toggling all-access + caching
  Unnecessary parens
  Can't revoke access to all-access bucket
  ...
This commit is contained in:
Jose Farias
2024-12-16 13:50:55 -06:00
29 changed files with 223 additions and 91 deletions
+16 -19
View File
@@ -78,6 +78,7 @@
--rotation: 45deg;
aspect-ratio: 6/9;
border-radius: var(--bubble-shape);
display: flex;
font-size: 6cqi;
inset: 70cqi 7cqi auto auto;
@@ -86,22 +87,27 @@
place-items: center;
transform: rotate(var(--rotation));
span {
.boost__form {
transform: rotate(calc(var(--rotation) * -1));
}
form {
block-size: 100%;
display: grid;
inline-size: 100%;
.boost__btn {
--btn-background: var(--bubble-color);
--btn-border-radius: 50%;
--btn-color: var(--color-ink-reversed);
--btn-size: 0.5em;
--btn-padding: 0.3em 0.5em;
font-weight: 800;
}
.btn {
--btn-background: transparent;
--btn-color: var(--bubble-color);
appearance: none;
.boost__input {
color: var(--bubble-color);
field-sizing: content;
font-weight: 800;
padding: 0;
text-align: center;
}
&.boosting {
@@ -210,15 +216,6 @@
transition: transform 200ms ease-out;
white-space: nowrap;
z-index: 1;
&:has(.input:not([type="file"], [type="date"], select)) {
border-radius: 2em;
padding: 0.2em 0.5em;
.input {
color: var(--bubble-color);
}
}
}
@media (hover: hover) {
+13
View File
@@ -0,0 +1,13 @@
.toggler--toggled {
.toggler__visible-when-off {
display: none;
}
.toggler__visible-when-on {
display: unset;
}
}
.toggler__visible-when-on {
display: none;
}
+10 -1
View File
@@ -2,6 +2,15 @@ class BoostsController < ApplicationController
include BubbleScoped, BucketScoped
def create
@bubble.boost!
count = if params[:boost_count].to_i == @bubble.boosts_count
@bubble.boosts_count + 1
else
params[:boost_count].to_i
end
@bubble.boost!(count)
respond_to do |format|
format.turbo_stream
end
end
end
+1 -1
View File
@@ -38,7 +38,7 @@ class BucketsController < ApplicationController
end
def bucket_params
params.expect(bucket: [ :name ])
params.expect(bucket: [ :name, :all_access ]).with_defaults(all_access: false)
end
def grantees
+15
View File
@@ -0,0 +1,15 @@
module AccessesHelper
def access_menu_tag(bucket, &)
tag.menu class: [ "flex flex-column gap margin-none pad txt-medium", { "toggler--toggled": bucket.all_access? } ], data: {
controller: "filter toggle-class",
filter_active_class: "filter--active", filter_selected_class: "selected",
toggle_class_toggle_class: "toggler--toggled" }, &
end
def access_toggles_for(users, selected:)
render partial: "buckets/access_toggle",
collection: users, as: :user,
locals: { selected: selected },
cached: ->(user) { [ user, selected ] }
end
end
+6 -3
View File
@@ -5,10 +5,13 @@ module Bubble::Boostable
scope :ordered_by_boosts, -> { order boosts_count: :desc }
end
def boost!
def boost!(count = 1)
count = count.to_i
count = 1 if count < 1
transaction do
track_event :boosted
increment! :boosts_count
track_event :boosted, count: count
update! boosts_count: count
rescore
end
end
+9 -1
View File
@@ -15,12 +15,20 @@ module Bucket::Accessible
end
def revoke_from(users)
destroy_by user: users
destroy_by user: users unless proxy_association.owner.all_access?
end
end
has_many :users, through: :accesses
scope :all_access, -> { where(all_access: true) }
after_create -> { accesses.grant_to creator }
after_save_commit :grant_access_to_everyone
end
private
def grant_access_to_everyone
accesses.grant_to(account.users) if all_access_previously_changed?(to: true)
end
end
+6
View File
@@ -12,5 +12,11 @@ module Filter::Resources
kind = resource.class.model_name.plural
send "#{kind}=", send(kind).without(resource)
empty? ? destroy! : save!
rescue ActiveRecord::RecordNotUnique
destroy!
end
def buckets
creator.buckets.where id: bucket_ids
end
end
+6
View File
@@ -21,6 +21,8 @@ class User < ApplicationRecord
validates_presence_of :email_address
normalizes :email_address, with: ->(value) { value.strip.downcase }
after_create_commit :grant_access_to_buckets
scope :active, -> { where(active: true) }
scope :alphabetically, -> { order("LOWER(name)") }
@@ -48,4 +50,8 @@ class User < ApplicationRecord
def deactived_email_address
email_address.sub(/@/, "-deactivated-#{SecureRandom.uuid}@")
end
def grant_access_to_buckets
Access.insert_all account.buckets.all_access.pluck(:id).collect { |bucket_id| { bucket_id: bucket_id, user_id: id } }
end
end
+5 -5
View File
@@ -5,10 +5,10 @@
toggle_class_toggle_class: "boosting",
action: "animationend->toggle-class#toggle" },
hidden: !bubble.boosts_count.positive? do %>
<%= button_to bucket_bubble_boosts_path(bubble.bucket, bubble),
class: "btn btn--plain",
data: { action: "toggle-class#toggle" },
form_class: "full-width" do %>
<%= tag.span("+ #{bubble.boosts_count}") if bubble.boosts_count.positive? %>
<%= form_with url: bucket_bubble_boosts_path(bubble.bucket, bubble),
class: "boost__form",
data: { action: "toggle-class#toggle" } do %>
<%= text_field_tag :boost_count, bubble.boosts_count, min: 1, class: "input borderless boost__input", autocomplete: "off" %>
<%= tag.button "+", class: "btn btn--plain boost__btn", type: :submit %>
<% end %>
<% end %>
+4 -4
View File
@@ -8,12 +8,12 @@
<h1 class="bubble__title">
<%= turbo_frame_tag bubble, :edit do %>
<%= link_to bubble.title, edit_bucket_bubble_path(bubble.bucket, bubble), class: "txt-undecorated" %>
<div class="bubble__tags flex flex-wrap align-end justify-center gap-half txt-tight-lines">
<%= render "bubbles/tags", bubble: bubble %>
</div>
<% end %>
</h1>
<div class="bubble__tags flex flex-wrap align-end justify-center gap-half txt-tight-lines">
<%= render "bubbles/tags", bubble: bubble %>
</div>
</div>
<div class="bubble__shape"></div>
+1 -1
View File
@@ -5,6 +5,6 @@
data-created-by-current-user-mine-class="comment--mine">
<%# Template Dependency: comments/comment %>
<%# Template Dependency: event_summaries/event_summary %>
<%= render bubble.messages, cache: true %>
<%= render bubble.messages, cached: true %>
<%= render "comments/new", bubble: bubble %>
</section>
+1 -1
View File
@@ -1,6 +1,6 @@
<% bubble.tags.each do |tag| %>
<%= link_to bubbles_path(@filter.to_params.merge(tag_ids: [ tag.id ])),
class: "bubble__tag btn btn--plain fill-transparent min-width", style: "color: #{bubble.color}" do %>
class: "bubble__tag btn btn--plain fill-transparent min-width", style: "color: #{bubble.color}", data: { turbo_frame: "_top" } do %>
<span class="overflow-ellipsis"><%= tag.hashtag %></span>
<% end %>
<% end %>
+5 -2
View File
@@ -1,12 +1,15 @@
<%= turbo_frame_tag @bubble, :edit do %>
<%= form_with model: @bubble, url: bucket_bubble_path(@bubble.bucket, @bubble), class: "flex flex-column gap full-width", data: { controller: "form" } do |form| %>
<%= form_with model: @bubble, url: bucket_bubble_path(@bubble.bucket, @bubble), class: "flex flex-column justify-center gap full-width", data: { controller: "form" } do |form| %>
<%= form.label :title, class: "flex flex-column justify-center align-center" do %>
<%= form.text_area :title, class: "input input--textara txt-align-center full-width borderless",
required: true, rows: 5, autofocus: true, placeholder: "Name it…",
data: { action: "keydown.ctrl+enter->form#submit:prevent keydown.meta+enter->form#submit:prevent keydown.esc->form#cancel focus->form#select" },
style: "color: var(--spat-color); --input-border-radius: 0" %>
<% end %>
<%= form.submit "Save", hidden: true %>
<%= form.button "Save", class: "btn btn--reversed txt-small center", type: :submit do %>
<%= image_tag "check.svg", aria: { hidden: true }, size: 24 %>
<span class="for-screen-reader">Save</span>
<% end %>
<%= link_to "Close editor and discard changes", bucket_bubble_path(@bubble.bucket, @bubble), data: { form_target: "cancel" }, hidden: true %>
<% end %>
<% end %>
+21
View File
@@ -0,0 +1,21 @@
<li class="flex align-center gap-half margin-none unpad" data-value="<%= user.name.downcase %>">
<figure class="avatar flex-item-no-shrink">
<%= avatar_tag user, loading: :lazy %>
</figure>
<div class="min-width">
<div class="overflow-ellipsis">
<strong><%= user.name %></strong>
</div>
</div>
<hr class="separator--horizontal flex-item-grow" style="--border-color: var(--color-subtle-dark); --border-style: dashed" aria-hidden="true">
<%= image_tag "check.svg", size: 20, class: "toggler__visible-when-on colorize--black flex-item-no-shrink", aria: { hidden: "true" } %>
<label class="switch toggler__visible-when-off flex-item-no-shrink">
<%= check_box_tag "user_ids[]", user.id, selected, class: "switch__input", id: nil %>
<span class="switch__btn round"></span>
<span class="for-screen-reader">Give <%= user.name %> access</span>
</label>
</li>
-24
View File
@@ -1,24 +0,0 @@
<li class="flex align-center gap-half margin-none unpad" data-value="<%= user.name.downcase %>">
<figure class="avatar flex-item-no-shrink">
<%= avatar_tag user, loading: :lazy %>
</figure>
<div class="min-width">
<div class="overflow-ellipsis">
<strong><%= user.name %></strong>
</div>
</div>
<hr class="separator--horizontal flex-item-grow" style="--border-color: var(--color-subtle-dark); --border-style: dashed" aria-hidden="true">
<% if user == Current.user %>
<%= hidden_field_tag "user_ids[]", user.id, id: nil %>
<%= image_tag "check.svg", size: 20, class: "colorize--black flex-item-no-shrink", aria: { hidden: "true" } %>
<% else %>
<label class="switch flex-item-no-shrink">
<%= check_box_tag "user_ids[]", user.id, selected, class: "switch__input", id: nil %>
<span class="switch__btn round"></span>
<span class="for-screen-reader">Give <%= user.name %> access</span>
</label>
<% end %>
</li>
-14
View File
@@ -1,14 +0,0 @@
<h1>Users on <%= @bucket.name %></h1>
<%= form_with url: bucket_access_path, method: :put do |form| %>
<ul>
<% Current.account.users.active.each do |user| %>
<li>
<%= check_box_tag "user_ids[]", user.id, @bucket.users.include?(user), id: nil, disabled: user == Current.user %>
<%= user.name %>
</li>
<% end %>
</ul>
<%= form.submit %>
<% end %>
+10 -12
View File
@@ -17,7 +17,7 @@
<% end %>
<div class="panel shadow center" style="--panel-size: 55ch;">
<%= form_with model: @bucket, class: "flex flex-column gap txt-large", controller: "form" do |form| %>
<%= form_with model: @bucket, class: "flex flex-column gap txt-large", data: { controller: "form" } do |form| %>
<div class="flex align-center gap">
<%= translation_button(:bucket_name) %>
@@ -29,7 +29,7 @@
</div>
<section class="margin-block pad-inline fill-shade border-radius">
<menu class="flex flex-column gap margin-none pad txt-medium" data-controller="filter" data-filter-active-class="filter--active" data-filter-selected-class="selected">
<%= access_menu_tag @bucket do %>
<li class="flex align-center gap margin-none">
<figure class="avatar flex-item-no-shrink" style="--avatar-border-radius: 0; --avatar-size: 4ch;">
<%= image_tag "everyone.svg", aria: { hidden: "true" }, class: "colorize--black" %>
@@ -42,13 +42,11 @@
<hr class="separator--horizontal flex-item-grow" style="--border-color: var(--color-subtle-dark); --border-style: dashed" aria-hidden="true" />
<%= link_to nil, class: "btn--faux flex-inline", tabindex: "-1" do %>
<label class="switch">
<input type="checkbox" lass="switch__input">
<span class="switch__btn round"></span>
<span class="for-screen-reader">Give everyone access to this project</span>
</label>
<% end %>
<label for="bucket_all_access" class="switch">
<%= form.check_box :all_access, class: "switch__input", checked: @bucket.all_access?, data: { action: "change->toggle-class#toggle" } %>
<span class="switch__btn round"></span>
<span class="for-screen-reader">Give everyone access to this project</span>
</label>
</li>
<hr class="separator--horizontal full-width" style="--border-color: var(--color-subtle-dark);" aria-hidden="true" />
@@ -58,15 +56,15 @@
<% end %>
<div data-filter-target="list">
<%= render partial: "buckets/user_toggle", collection: @selected_users, as: :user, locals: { selected: true } %>
<%= access_toggles_for @selected_users, selected: true %>
<% if @selected_users.any? && @unselected_users.any? %>
<hr class="separator full-width" style="--border-style: solid">
<% end %>
<%= render partial: "buckets/user_toggle", collection: @unselected_users, as: :user, locals: { selected: false } %>
<%= access_toggles_for @unselected_users, selected: false %>
</div>
</menu>
<% end %>
</section>
<button type="submit" id="log_in" class="btn btn--reversed center">
@@ -0,0 +1,5 @@
class AddAllAccessToBucket < ActiveRecord::Migration[8.1]
def change
add_column :buckets, :all_access, :boolean, default: false, null: false
end
end
Generated
+2 -1
View File
@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[8.0].define(version: 2024_11_26_232658) do
ActiveRecord::Schema[8.1].define(version: 2024_12_09_223551) do
create_table "accesses", force: :cascade do |t|
t.integer "bucket_id", null: false
t.integer "user_id", null: false
@@ -116,6 +116,7 @@ ActiveRecord::Schema[8.0].define(version: 2024_11_26_232658) do
t.string "name", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.boolean "all_access", default: false, null: false
t.index ["account_id"], name: "index_buckets_on_account_id"
t.index ["creator_id"], name: "index_buckets_on_creator_id"
end
+13 -1
View File
@@ -6,8 +6,20 @@ class BoostsControllerTest < ActionDispatch::IntegrationTest
end
test "create" do
boost_count = bubbles(:logo).boosts_count
assert_difference "bubbles(:logo).reload.boosts_count", +1 do
post bucket_bubble_boosts_url(buckets(:writebook), bubbles(:logo), format: :turbo_stream)
post bucket_bubble_boosts_url(buckets(:writebook), bubbles(:logo), params: { boost_count: boost_count }, format: :turbo_stream)
end
assert_turbo_stream action: :update, target: dom_id(bubbles(:logo), :boosts)
end
test "create with value" do
boost_count = 10
assert_changes "bubbles(:logo).reload.boosts_count", to: boost_count do
post bucket_bubble_boosts_url(buckets(:writebook), bubbles(:logo), params: { boost_count: boost_count }, format: :turbo_stream)
end
assert_turbo_stream action: :update, target: dom_id(bubbles(:logo), :boosts)
@@ -49,4 +49,14 @@ class BubblesControllerTest < ActionDispatch::IntegrationTest
assert_equal "moon.jpg", bubble.image.filename.to_s
assert_equal [ tags(:mobile) ], bubble.tags
end
test "users can only see bubbles in buckets they have access to" do
get bucket_bubble_url(buckets(:writebook), bubbles(:logo))
assert_response :success
buckets(:writebook).update! all_access: false
buckets(:writebook).accesses.revoke_from users(:kevin)
get bucket_bubble_url(buckets(:writebook), bubbles(:logo))
assert_response :not_found
end
end
@@ -37,6 +37,20 @@ class BucketsControllerTest < ActionDispatch::IntegrationTest
assert_redirected_to bubbles_path(bucket_ids: [ buckets(:writebook) ])
assert_equal "Writebook bugs", buckets(:writebook).reload.name
assert_equal users(:david, :jz), buckets(:writebook).users
assert_not buckets(:writebook).all_access?
end
test "update all access" do
bucket = Current.set(session: sessions(:kevin)) do
accounts("37s").buckets.create! name: "New bucket", all_access: false
end
assert_equal [ users(:kevin) ], bucket.users
patch bucket_url(bucket), params: { bucket: { name: "Bugs", all_access: true } }
assert_redirected_to bubbles_path(bucket_ids: [ bucket ])
assert bucket.reload.all_access?
assert_equal accounts("37s").users, bucket.users
end
test "destroy" do
+1
View File
@@ -2,3 +2,4 @@ writebook:
name: Writebook
account: 37s
creator: david
all_access: true
+3
View File
@@ -1,2 +1,5 @@
david:
user: david
kevin:
user: kevin
+1 -1
View File
@@ -15,7 +15,7 @@ class BubbleTest < ActiveSupport::TestCase
test "boosting" do
assert_difference %w[ bubbles(:logo).boosts_count bubbles(:logo).activity_score Event.count ], +1 do
bubbles(:logo).boost!
bubbles(:logo).boost!(bubbles(:logo).boosts_count+ 1)
end
end
+19
View File
@@ -2,6 +2,8 @@ require "test_helper"
class BucketTest < ActiveSupport::TestCase
test "revising access" do
buckets(:writebook).update! all_access: false
buckets(:writebook).accesses.revise granted: users(:david, :jz), revoked: users(:kevin)
assert_equal users(:david, :jz), buckets(:writebook).users
@@ -11,4 +13,21 @@ class BucketTest < ActiveSupport::TestCase
buckets(:writebook).accesses.revoke_from users(:kevin)
assert_not_includes buckets(:writebook).users.reload, users(:kevin)
end
test "grants access to everyone after creation" do
bucket = Current.set(session: sessions(:david)) do
accounts("37s").buckets.create! name: "New bucket", all_access: true
end
assert_equal accounts("37s").users, bucket.users
end
test "grants access to everyone after update" do
bucket = Current.set(session: sessions(:david)) do
accounts("37s").buckets.create! name: "New bucket"
end
assert_equal [ users(:david) ], bucket.users
bucket.update! all_access: true
assert_equal accounts("37s").users, bucket.users.reload
end
end
+16
View File
@@ -41,6 +41,13 @@ class FilterTest < ActiveSupport::TestCase
assert_equal [ bubbles(:shipping) ], filter.bubbles
end
test "can't see bubbles in buckets that aren't accessible" do
buckets(:writebook).update! all_access: false
buckets(:writebook).accesses.revoke_from users(:david)
assert_empty users(:david).filters.new(bucket_ids: [ buckets(:writebook).id ]).bubbles
end
test "turning into params" do
expected = { indexed_by: "most_discussed", tag_ids: [ tags(:mobile).id ], assignee_ids: [ users(:jz).id ], filter_id: filters(:jz_assignments).id }
assert_equal expected.stringify_keys, filters(:jz_assignments).to_params.to_h
@@ -92,6 +99,15 @@ class FilterTest < ActiveSupport::TestCase
end
end
test "duplicate filters are removed after a resource is destroyed" do
users(:david).filters.create! tag_ids: [ tags(:mobile).id ], bucket_ids: [ buckets(:writebook).id ]
users(:david).filters.create! tag_ids: [ tags(:mobile).id, tags(:web).id ], bucket_ids: [ buckets(:writebook).id ]
assert_difference "Filter.count", -1 do
tags(:web).destroy!
end
end
test "summary" do
assert_equal "Most discussed, tagged #Mobile, and assigned to JZ in all projects", filters(:jz_assignments).summary
end
+10
View File
@@ -12,6 +12,16 @@ class UserTest < ActiveSupport::TestCase
assert_equal user, User.authenticate_by(email_address: "victor@hey.com", password: "secret123456")
end
test "creation gives access to all_access buckets" do
user = User.create! \
account: accounts("37s"),
name: "Victor Cooper",
email_address: "victor@hey.com",
password: "secret123456"
assert_equal [ buckets(:writebook) ], user.buckets
end
test "deactivate" do
assert_changes -> { users(:jz).active? }, from: true, to: false do
users(:jz).deactivate