Merge branch 'main' into cascade-layers-2

* main: (104 commits)
  Default collection permissions to "all access"
  dep: update active_record-tenanted to 8758240b
  dep: bump Rails to 32d96235
  Test for `Bubbles::RecoversController`
  Move readings under bubbles and out of double nesting
  Move bubble stagings out from double nesting
  Correct alpha ordering
  Fix indention
  Move bubble recovery out from double nesting
  Move bubbles publish out from the double nesting
  Move bubble pops out from double nesting
  Move controller to delete bubble image out from double nesting under bucket
  Revert "Scope searches by user"
  Scope searches by user
  Few more lazy loading avatars!
  Another lazy loading point
  Remove double nesting under bubbles for engagement
  Lets just preload all avatars for now
  No need for an array when there is only one dependency
  Scope data by account
  ...
This commit is contained in:
Andy Smith
2025-04-07 14:15:55 -05:00
123 changed files with 706 additions and 734 deletions
-93
View File
@@ -1,93 +0,0 @@
name: CI
on:
pull_request:
push:
branches: [ main ]
env:
BUNDLE_GITHUB__COM: x-access-token:${{ secrets.GH_TOKEN }}
jobs:
scan_ruby:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: .ruby-version
bundler-cache: true
- name: Scan for security vulnerabilities in Ruby dependencies
run: bin/brakeman --no-pager
scan_js:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: .ruby-version
bundler-cache: true
- name: Scan for security vulnerabilities in JavaScript dependencies
run: bin/importmap audit
lint:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: .ruby-version
bundler-cache: true
- name: Lint code for consistent style
run: bin/rubocop -f github
test:
runs-on: ubuntu-latest
# services:
# redis:
# image: redis
# ports:
# - 6379:6379
# options: --health-cmd "redis-cli ping" --health-interval 10s --health-timeout 5s --health-retries 5
steps:
- name: Install packages
run: sudo apt-get update && sudo apt-get install --no-install-recommends -y google-chrome-stable curl libjemalloc2 libsqlite3-0 libvips
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: .ruby-version
bundler-cache: true
- name: Run tests
env:
RAILS_ENV: test
# REDIS_URL: redis://localhost:6379/0
run: bin/rails db:setup test test:system
- name: Keep screenshots from failed system tests
uses: actions/upload-artifact@v4
if: failure()
with:
name: screenshots
path: ${{ github.workspace }}/tmp/screenshots
if-no-files-found: ignore
+1 -5
View File
@@ -1,6 +1,5 @@
source "https://rubygems.org"
git_source(:bc) { |repo| "https://github.com/basecamp/#{repo}" }
ruby file: ".ruby-version"
gem "rails", github: "rails/rails", branch: "main"
gem "active_record-tenanted", bc: "active_record-tenanted"
@@ -36,14 +35,11 @@ gem "rails_structured_logging", bc: "rails-structured-logging"
group :development, :test do
gem "debug"
gem "bundler-audit", require: false
gem "brakeman", require: false
gem "rubocop-rails-omakase", require: false
end
group :development do
gem "hotwire-spark"
end
group :test do
gem "capybara"
gem "selenium-webdriver"
+9 -29
View File
@@ -1,6 +1,6 @@
GIT
remote: https://github.com/basecamp/active_record-tenanted
revision: 1bb339a1ca045a240aefacc2bb0b88607935d401
revision: 8758240be33a6f6f7e7f56486a2e78008c193030
specs:
active_record-tenanted (0.1.0)
activerecord (>= 8.1.alpha)
@@ -18,7 +18,7 @@ GIT
GIT
remote: https://github.com/rails/rails.git
revision: e0452e80137c396c93465cf98f8eda43026346c7
revision: 32d96235810e268eb5335a7837b13cda3f8b653d
branch: main
specs:
actioncable (8.1.0.alpha)
@@ -128,9 +128,12 @@ GEM
bigdecimal (3.1.9)
bootsnap (1.18.4)
msgpack (~> 1.2)
brakeman (7.0.0)
brakeman (7.0.2)
racc
builder (3.3.0)
bundler-audit (0.9.2)
bundler (>= 1.2.0, < 3)
thor (~> 1.0)
capybara (3.40.0)
addressable
matrix
@@ -154,16 +157,6 @@ GEM
erubi (1.13.1)
et-orbi (1.2.11)
tzinfo
ffi (1.17.1-aarch64-linux-gnu)
ffi (1.17.1-aarch64-linux-musl)
ffi (1.17.1-arm-linux-gnu)
ffi (1.17.1-arm-linux-musl)
ffi (1.17.1-arm64-darwin)
ffi (1.17.1-x86-linux-gnu)
ffi (1.17.1-x86-linux-musl)
ffi (1.17.1-x86_64-darwin)
ffi (1.17.1-x86_64-linux-gnu)
ffi (1.17.1-x86_64-linux-musl)
fugit (1.11.1)
et-orbi (~> 1, >= 1.2.11)
raabro (~> 1.4)
@@ -172,10 +165,6 @@ GEM
addressable (>= 2.5.0)
globalid (1.2.1)
activesupport (>= 6.1)
hotwire-spark (0.1.13)
listen
rails (>= 7.0.0)
zeitwerk
i18n (1.14.7)
concurrent-ruby (~> 1.0)
importmap-rails (2.1.0)
@@ -204,9 +193,6 @@ GEM
zeitwerk (>= 2.6.18, < 3.0)
language_server-protocol (3.17.0.4)
lint_roller (1.1.0)
listen (3.9.0)
rb-fsevent (~> 0.10, >= 0.10.3)
rb-inotify (~> 0.9, >= 0.9.10)
logger (1.7.0)
loofah (2.24.0)
crass (~> 1.0.2)
@@ -297,14 +283,11 @@ GEM
nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0)
rainbow (3.1.1)
rake (13.2.1)
rb-fsevent (0.11.2)
rb-inotify (0.11.1)
ffi (~> 1.0)
rdoc (6.13.1)
psych (>= 4.0.0)
redcarpet (3.6.1)
regexp_parser (2.10.0)
reline (0.6.0)
reline (0.6.1)
io-console (~> 0.5)
rexml (3.4.1)
rouge (4.5.1)
@@ -343,7 +326,7 @@ GEM
ruby-progressbar (1.13.0)
rubyzip (2.4.1)
securerandom (0.4.1)
selenium-webdriver (4.30.1)
selenium-webdriver (4.31.0)
base64 (~> 0.2)
logger (~> 1.4)
rexml (~> 3.2, >= 3.2.5)
@@ -438,10 +421,10 @@ DEPENDENCIES
bcrypt (~> 3.1.7)
bootsnap
brakeman
bundler-audit
capybara
debug
geared_pagination (~> 1.2)
hotwire-spark
importmap-rails
jbuilder
kamal
@@ -464,8 +447,5 @@ DEPENDENCIES
thruster
turbo-rails
RUBY VERSION
ruby 3.4.2p28
BUNDLED WITH
2.5.21
+1 -1
View File
@@ -1 +1 @@
<svg viewBox="0 0 24 20" xmlns="http://www.w3.org/2000/svg"><path d="m19.1 0c-2.8 0-5 2.2-5 5s2.2 5 5 5 5-2.2 5-5-2.2-5-5-5zm-.1 2.5c.3 0 .6.3.6.6v1.2c0 .2 0 .2.2.2h1.2c.3 0 .6.3.6.6s-.3.6-.6.6h-1.2c-.2 0-.2 0-.2.2v1.2c0 .3-.3.6-.6.6s-.6-.3-.6-.6v-1.2c0-.2 0-.2-.2-.2h-1.2c-.3 0-.6-.3-.6-.6s.3-.6.6-.6h1.2c.2 0 .2 0 .2-.2v-1.2c0-.3.3-.6.6-.6z"/><path d="m6.7 10.5c.9 0 1.6-.7 1.6-1.6s-.7-1.6-1.6-1.6-1.6.7-1.6 1.6.7 1.6 1.6 1.6z" fill="none"/><path d="m9.7 16.9h.1l-.1-.1z"/><path d="m19.2 11.6v6.3c0 .2 0 .4-.4.4h-16.8v-13.7c0-.2 0-.4.4-.4h10.1c0-.7.3-1.3.5-1.8h-11.6c-.9.2-1.4.9-1.4 1.8v14c0 1 .8 1.8 1.8 1.8h17.5c1 0 1.8-.8 1.8-1.8v-6.9c-.6.2-1.2.3-1.8.3z"/><path d="m4.8 5.5c-.7 0-1.3.6-1.3 1.3v8.9c0 .4.2.7.5 1l2.6-3.5c.2-.3.6-.5.9-.5s0 0 0 0c.4 0 .7.2.9.5l.9 1.3 2.3-3.5c.3-.4.8-.7 1.3-.7s0 0 0 0c.5 0 1 .3 1.3.7l3.1 5c0-.1 0-.2 0-.3v-4.2c-2.8-.6-4.9-3-5.1-6h-7.6zm1.9 5c-.9 0-1.6-.7-1.6-1.6s.7-1.6 1.6-1.6 1.6.7 1.6 1.6-.7 1.6-1.6 1.6z"/><path d="m5.2 16.9h2.6l1-1.5-1.2-1.7z"/><path d="m10.4 15.8-.7 1 .1.1h1.3 5.1l-3.1-5.1z"/></svg>
<svg width="24" height="24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M19 2c-2.8 0-5 2.2-5 5s2.2 5 5 5 5-2.2 5-5-2.2-5-5-5zm-.1 2.5c.3 0 .6.3.6.6v1.2c0 .2 0 .2.2.2h1.2c.3 0 .6.3.6.6s-.3.6-.6.6h-1.2c-.2 0-.2 0-.2.2v1.2c0 .3-.3.6-.6.6s-.6-.3-.6-.6V7.9c0-.2 0-.2-.2-.2h-1.2c-.3 0-.6-.3-.6-.6s.3-.6.6-.6h1.2c.2 0 .2 0 .2-.2V5.1c0-.3.3-.6.6-.6zM9.7 18.9h.1l-.1-.1v.1z" fill="#000"/><path d="M19.2 13.6v6.3c0 .2 0 .4-.4.4H2V6.6c0-.2 0-.4.4-.4h10.1c0-.7.3-1.3.5-1.8H1.4C.5 4.6 0 5.3 0 6.2v14c0 1 .8 1.8 1.8 1.8h17.5c1 0 1.8-.8 1.8-1.8v-6.9c-.6.2-1.2.3-1.8.3h-.1z" fill="#000"/><path d="M4.8 7.5c-.7 0-1.3.6-1.3 1.3v8.9c0 .4.2.7.5 1l2.6-3.5c.2-.3.6-.5.9-.5.4 0 .7.2.9.5l.9 1.3 2.3-3.5c.3-.4.8-.7 1.3-.7s1 .3 1.3.7l3.1 5v-4.5c-2.8-.6-4.9-3-5.1-6H4.6h.2zm1.9 5c-.9 0-1.6-.7-1.6-1.6 0-.9.7-1.6 1.6-1.6.9 0 1.6.7 1.6 1.6 0 .9-.7 1.6-1.6 1.6z" fill="#000"/><path d="M5.2 18.9h2.6l1-1.5-1.2-1.7-2.4 3.2zM10.4 17.8l-.7 1 .1.1H16.2l-3.1-5.1-2.7 4z" fill="#000"/></svg>

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 966 B

+1
View File
@@ -0,0 +1 @@
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="m12 18.5 7.5 4.5-2-8.5 6.6-5.7-8.7-.8-3.4-8-3.4 8-8.7.8 6.6 5.7-2 8.5z"/></svg>

After

Width:  |  Height:  |  Size: 148 B

+25 -4
View File
@@ -33,10 +33,31 @@
}
}
&:where(.avatar) {
--btn-border-radius: var(--avatar-border-radius);
--btn-padding: 0;
--btn-border-size: 0;
/* Circle buttons */
&.btn--circle,
&[aria-label]:where(:has(.icon)),
&:where(:has(.for-screen-reader):has(.icon)) {
--btn-border-radius: 50%;
--btn-padding: 0;
aspect-ratio: 1;
block-size: var(--btn-size);
display: grid;
inline-size: var(--btn-size);
place-items: center;
> * {
grid-area: 1/1;
}
}
/* With radios and checkboxes */
&:has(input[type=radio]),
&:has(input[type=checkbox]) {
position: relative;
:is(input[type=radio]),
:is(input[type=checkbox]) {
--hover-size: 0;
aspect-ratio: 1;
+36 -4
View File
@@ -73,7 +73,9 @@
}
.card {
--border-color: transparent;
--border-radius: 0.2em;
--border-size: 0;
aspect-ratio: 2/0.95;
background-color: color-mix(in srgb, var(--bubble-color) 4%, var(--color-bg));
@@ -84,6 +86,15 @@
.card__container & {
inline-size: calc(100dvw - var(--actions-inline-inset) * 2 - var(--btn-size) * 2);
}
.local-time-value {
font-weight: bold;
}
@media (prefers-color-scheme: dark) {
--border-color: var(--color-subtle-dark);
--border-size: 1px;
}
}
.card__actions {
@@ -127,6 +138,21 @@
}
}
.card__actions-container--top {
margin: var(--block-space-half) auto calc(var(--block-space) * -2);
padding: var(--block-space) 3ch;
#header:has(&) {
position: relative;
z-index: 1;
}
.switch__input:checked + .switch__btn {
background-color: var(--bubble-color) !important;
}
}
.cards .card__assignees:not(:has(.avatar)) {
.txt-uppercase {
display: none;
@@ -206,11 +232,17 @@
--padding-inline: calc(var(--block-space-double) + var(--block-space));
--padding-block: var(--block-space-double) calc(var(--block-space-double) + var(--block-space-half));
background-color: var(--color-container);
background-color: color-mix(in srgb, var(--bubble-color) 33%, var(--color-bg));
border-radius: 0.2em;
display: inline-flex;
padding-block: var(--padding-block);
padding-inline: var(--padding-inline);
&:has(.card__star-input:checked) {
.card {
outline: 4px solid var(--color-negative);
}
}
}
.card__container--pointing {
@@ -346,12 +378,12 @@
.card__meta {
--border-color: var(--bubble-color);
--border-size: 1px;
--row-gap: 0.25em;
margin-inline-start: var(--inline-space-double);
strong {
font-size: 1.1em;
font-weight: 900;
}
@@ -462,9 +494,9 @@
font-size: var(--text-rel-large);
font-weight: 900;
text-shadow:
-2px -2px 0 var(--bubble-color),
-2px -2px 0 var(--bubble-color),
2px -2px 0 var(--bubble-color),
-2px 2px 0 var(--bubble-color),
-2px 2px 0 var(--bubble-color),
2px 2px 0 var(--bubble-color);
}
}
+1 -1
View File
@@ -11,7 +11,7 @@
place-items: center;
text-align: center;
@media (min-width: 150ch) {
@media (min-width: 160ch) {
padding-inline: calc(var(--tray-size) + var(--inline-space) * 1.5);
}
}
+1
View File
@@ -76,6 +76,7 @@
.icon--share { --svg: url("share.svg "); }
.icon--sort-ascending { --svg: url("sort-ascending.svg "); }
.icon--sort-descending { --svg: url("sort-descending.svg "); }
.icon--star { --svg: url("star.svg "); }
.icon--tag { --svg: url("tag.svg "); }
.icon--temperature { --svg: url("temperature.svg "); }
.icon--thumb-up { --svg: url("thumb-up.svg "); }
-1
View File
@@ -37,7 +37,6 @@
view-transition-name: header;
> nav {
font-size: var(--text-small);
view-transition-name: nav;
header {
@@ -1,34 +0,0 @@
class Assignments::TogglesController < ApplicationController
include BubbleScoped, BucketScoped
def new
render partial: "bubbles/assignment", locals: { bubble: @bubble }
end
def create
new_assignee_ids = Array(params[:assignee_id])
current_assignees = @bubble.assignees
current_assignees.each do |assignee|
@bubble.toggle_assignment(assignee) unless new_assignee_ids.include?(assignee.id.to_s)
end
new_assignee_ids.each do |id|
assignee = @bucket.users.active.find(id)
@bubble.toggle_assignment(assignee) unless current_assignees.include?(assignee)
end
@bubble.assignees.reload
respond_to do |format|
format.turbo_stream do
render turbo_stream: turbo_stream.replace([ @bubble, :assignees ], partial: "bubbles/cards/perma/assignees", locals: { bubble: @bubble })
end
end
end
private
def assignee
@bucket.users.active.find params[:assignee_id]
end
end
@@ -0,0 +1,10 @@
class Bubbles::AssignmentsController < ApplicationController
include BubbleScoped
def new
end
def create
@bubble.toggle_assignment @bucket.users.active.find(params[:assignee_id])
end
end
@@ -1,5 +1,5 @@
class BoostsController < ApplicationController
include BubbleScoped, BucketScoped
class Bubbles::BoostsController < ApplicationController
include BubbleScoped
def create
count = if params[:boost_count].to_i == @bubble.boosts_count
@@ -8,9 +8,5 @@ class BoostsController < ApplicationController
params[:boost_count].to_i
end
@bubble.boost!(count)
respond_to do |format|
format.turbo_stream
end
end
end
@@ -1,5 +1,5 @@
class Bubbles::EngagementsController < ApplicationController
include BubbleScoped, BucketScoped
include BubbleScoped
def create
@bubble.engage
+1 -1
View File
@@ -1,5 +1,5 @@
class Bubbles::ImagesController < ApplicationController
include BubbleScoped, BucketScoped
include BubbleScoped
def destroy
@bubble.image.purge_later
+18 -5
View File
@@ -1,16 +1,29 @@
class Bubbles::PinsController < ApplicationController
include BubbleScoped, BucketScoped
include BubbleScoped
def show
end
def create
@bubble.set_pinned(Current.user, true)
redirect_to bucket_bubble_pin_path(@bucket, @bubble)
pin = @bubble.pin_by Current.user
broadcast_my_new pin
redirect_to bubble_pin_path(@bubble)
end
def destroy
@bubble.set_pinned(Current.user, false)
redirect_to bucket_bubble_pin_path(@bucket, @bubble)
pin = @bubble.unpin_by Current.user
broadcast_my_removed pin
redirect_to bubble_pin_path(@bubble)
end
private
def broadcast_my_new(pin)
pin.broadcast_prepend_later_to [ Current.user, :pins ], target: "pins", partial: "my/pins/pin"
end
def broadcast_my_removed(pin)
pin.broadcast_remove_to [ Current.user, :pins ]
end
end
+1 -1
View File
@@ -1,5 +1,5 @@
class Bubbles::PopsController < ApplicationController
include BubbleScoped, BucketScoped
include BubbleScoped
def create
@bubble.pop!(user: Current.user, reason: params[:reason])
@@ -1,5 +1,5 @@
class Bubbles::PublishesController < ApplicationController
include BubbleScoped, BucketScoped
include BubbleScoped
def create
@bubble.publish
@@ -1,5 +1,5 @@
class ReadingsController < ApplicationController
include BubbleScoped, BucketScoped
class Bubbles::ReadingsController < ApplicationController
include BubbleScoped
def create
mark_bubble_notifications_read
@@ -1,5 +1,5 @@
class Bubbles::RecoversController < ApplicationController
include BubbleScoped, BucketScoped
include BubbleScoped
def create
redirect_to @bubble.recover_abandoned_creation
@@ -1,5 +1,5 @@
class Bubbles::StagingsController < ApplicationController
include BubbleScoped, BucketScoped
include BubbleScoped
def create
if params[:stage_id].present?
@@ -0,0 +1,16 @@
class Bubbles::TaggingsController < ApplicationController
include BubbleScoped
def new
@tags = Current.account.tags.alphabetically
end
def create
@bubble.toggle_tag_with sanitized_tag_title_param
end
private
def sanitized_tag_title_param
params.required(:tag_title).strip.gsub(/\A#/, "")
end
end
@@ -1,17 +1,13 @@
class Bubbles::WatchesController < ApplicationController
include BubbleScoped, BucketScoped
include BubbleScoped
def create
set_watching_and_redirect(true)
@bubble.watch_by Current.user
redirect_to bubble_watch_path(@bubble)
end
def destroy
set_watching_and_redirect(false)
@bubble.unwatch_by Current.user
redirect_to bubble_watch_path(@bubble)
end
private
def set_watching_and_redirect(watching)
@bubble.set_watching(Current.user, watching)
redirect_to bucket_bubble_watch_path(@bucket, @bubble)
end
end
+1 -1
View File
@@ -33,7 +33,7 @@ class BucketsController < ApplicationController
end
def bucket_params
params.expect(bucket: [ :name, :all_access ]).with_defaults(all_access: false)
params.expect(bucket: [ :name, :all_access ]).with_defaults(all_access: true)
end
def grantees
@@ -24,7 +24,7 @@ class Comments::ReactionsController < ApplicationController
private
def set_comment
@comment = Comment.find(params[:comment_id])
@comment = Current.account.comments.find(params[:comment_id])
end
def reaction_params
+2 -2
View File
@@ -1,5 +1,5 @@
class CommentsController < ApplicationController
include BubbleScoped, BucketScoped
include BubbleScoped
before_action :set_comment, only: [ :show, :edit, :update, :destroy ]
before_action :require_own_comment, only: [ :edit, :update, :destroy ]
@@ -38,6 +38,6 @@ class CommentsController < ApplicationController
end
def require_own_comment
head :forbidden unless @comment.creator.current?
head :forbidden unless Current.user == @comment.creator
end
end
+6 -2
View File
@@ -2,11 +2,15 @@ module BubbleScoped
extend ActiveSupport::Concern
included do
before_action :set_bubble
before_action :set_bubble, :set_bucket
end
private
def set_bubble
@bubble = @bucket.bubbles.find(params[:bubble_id])
@bubble = Current.user.accessible_bubbles.find(params[:bubble_id])
end
def set_bucket
@bucket = Current.user.buckets.find(@bubble.bucket_id)
end
end
@@ -1,5 +1,5 @@
class Bubbles::Pins::TraysController < ApplicationController
def show
class My::PinsController < ApplicationController
def index
@pins = Current.user.pins.includes(:bubble).ordered.limit(20)
end
end
@@ -1,35 +0,0 @@
class Taggings::TogglesController < ApplicationController
include BubbleScoped, BucketScoped
def new
render partial: "bubbles/tagging", locals: { bubble: @bubble, tags: Current.account.tags }
end
def create
if params[:tag_title].present?
sanitized_title = params[:tag_title].strip.gsub(/\A#/, "")
tag = Current.account.tags.find_or_create_by!(title: sanitized_title)
@bubble.toggle_tag(tag)
else
new_tag_ids = Array(params[:tag_id])
current_tags = @bubble.tags
current_tags.each do |tag|
@bubble.toggle_tag(tag) unless new_tag_ids.include?(tag.id.to_s)
end
new_tag_ids.each do |id|
tag = Current.account.tags.find(id)
@bubble.toggle_tag(tag) unless current_tags.include?(tag)
end
end
@bubble.tags.reload
respond_to do |format|
format.turbo_stream do
render turbo_stream: turbo_stream.replace([ @bubble, :tags ], partial: "bubbles/cards/perma/tags", locals: { bubble: @bubble })
end
end
end
end
+9 -1
View File
@@ -4,7 +4,7 @@ class Users::AvatarsController < ApplicationController
before_action :set_user
def show
if stale? @user, cache_control: { max_age: 30.minutes, stale_while_revalidate: 1.week }
if stale? @user, cache_control: { max_age: cache_max_age, stale_while_revalidate: 1.week }
render_avatar_or_initials
end
end
@@ -15,6 +15,14 @@ class Users::AvatarsController < ApplicationController
end
private
def cache_max_age
if Current.user == @user
0
else
30.minutes
end
end
def set_user
@user = Current.account.users.find(params[:user_id])
end
+7
View File
@@ -6,4 +6,11 @@ module NavigationHelper
icon_tag(icon) + tag.span(label, class: "for-screen-reader")
end
end
def link_to_back(fallback_path: root_path)
link_to fallback_path, class: "btn flex-item-justify-start", aria: { label: "Go back" },
data: { controller: "back-navigation hotkey", action: "keydown.esc@document->hotkey#click", back_navigation_fallback_destination_value: fallback_path } do
icon_tag("arrow-left")
end
end
end
+5
View File
@@ -0,0 +1,5 @@
module UsersHelper
def prepend_current_user_to(users_scope)
users_scope.to_a.prepend(Current.user).uniq
end
end
+7 -7
View File
@@ -1,12 +1,12 @@
module WorkflowsHelper
def button_to_set_stage(bubble, stage)
button_to bucket_bubble_stagings_path(bubble.bucket, bubble, stage_id: stage),
method: :post, class: [ "btn full-width justify-start workflow-stage txt-uppercase", { "workflow-stage--current": stage == bubble.stage } ],
form_class: "flex align-center gap-half",
data: { turbo_frame: "_top" } do
tag.span class: "overflow-ellipsis" do
stage.name
end
button_to bubble_stagings_path(bubble, stage_id: stage),
method: :post, class: [ "btn full-width justify-start workflow-stage txt-uppercase", { "workflow-stage--current": stage == bubble.stage } ],
form_class: "flex align-center gap-half",
data: { turbo_frame: "_top" } do
tag.span class: "overflow-ellipsis" do
stage.name
end
end
end
end
@@ -1,7 +1,8 @@
import { Controller } from "@hotwired/stimulus"
import { differenceInDays } from "helpers/date_helpers"
export default class extends Controller {
static targets = [ "time", "date", "datetime", "shortdate", "ago", "indays" ]
static targets = [ "time", "date", "datetime", "shortdate", "ago", "indays", "daysago" ]
#timer
@@ -11,7 +12,7 @@ export default class extends Controller {
this.shortDateFormatter = new Intl.DateTimeFormat(undefined, { month: "short", day: "numeric" })
this.dateTimeFormatter = new Intl.DateTimeFormat(undefined, { timeStyle: "short", dateStyle: "short" })
this.agoFormatter = new AgoFormatter()
this.daysAgoFormatter = new DaysAgoFormatter()
this.daysagoFormatter = new DaysAgoFormatter()
this.indaysFormatter = new InDaysFormatter()
}
@@ -43,14 +44,14 @@ export default class extends Controller {
this.#formatTime(this.agoFormatter, target)
}
daysAgoTargetConnected(target) {
this.#formatTime(this.daysAgoFormatter, target)
}
indaysTargetConnected(target) {
this.#formatTime(this.indaysFormatter, target)
}
daysagoTargetConnected(target) {
this.#formatTime(this.daysagoFormatter, target)
}
#refreshRelativeTimes() {
this.agoTargets.forEach(target => {
this.#formatTime(this.agoFormatter, target)
@@ -59,7 +60,7 @@ export default class extends Controller {
#formatTime(formatter, target) {
const dt = new Date(target.getAttribute("datetime"))
target.textContent = formatter.format(dt)
target.innerHTML = formatter.format(dt)
target.title = this.dateTimeFormatter.format(dt)
}
}
@@ -93,38 +94,25 @@ class AgoFormatter {
}
class DaysAgoFormatter {
format(dt) {
const now = new Date()
format(date) {
const days = differenceInDays(date, new Date())
const startOfToday = new Date(now.getFullYear(), now.getMonth(), now.getDate())
const startOfGivenDay = new Date(dt.getFullYear(), dt.getMonth(), dt.getDate())
const msPerDay = 1000 * 60 * 60 * 24
const dayDiff = Math.floor((startOfToday - startOfGivenDay) / msPerDay)
if (dayDiff === 0) return "Today"
if (dayDiff === 1) return "Yesterday"
return `in ${dayDiff} days`
if (days <= 0) return styleableValue("today")
if (days === 1) return styleableValue("yesterday")
return `${styleableValue(days)} days ago`
}
}
class InDaysFormatter {
format(dt) {
const target = this.#beginningOfDay(dt)
const today = this.#beginningOfDay(new Date())
const days = Math.round((target - today) / (1000 * 60 * 60 * 24))
format(date) {
const days = differenceInDays(new Date(), date)
if (days <= 0) {
return "today"
}
if (days === 1) {
return "tomorrow"
}
return `in ${Math.round(days)} days`
}
#beginningOfDay(dt) {
return new Date(dt.getFullYear(), dt.getMonth(), dt.getDate())
if (days <= 0) return styleableValue("today")
if (days === 1) return styleableValue("tomorrow")
return `in ${styleableValue(days)} days`
}
}
function styleableValue(value) {
return `<span class="local-time-value">${value}</span>`
}
+7
View File
@@ -0,0 +1,7 @@
export function differenceInDays(fromDate, toDate) {
return Math.round(Math.abs((beginningOfDay(toDate) - beginningOfDay(fromDate)) / (1000 * 60 * 60 * 24)))
}
export function beginningOfDay(date) {
return new Date(date.getFullYear(), date.getMonth(), date.getDate())
}
+2
View File
@@ -10,6 +10,8 @@ class Account < ApplicationRecord
end
end
has_many :comments, through: :users
has_many :workflows, dependent: :destroy
has_many :stages, through: :workflows, class_name: "Workflow::Stage"
+1 -1
View File
@@ -21,7 +21,7 @@ module Bubble::Assignable
private
def assign(user)
assignments.create! assignee: user, assigner: Current.user
set_watching(user, true)
watch_by user
track_event :assigned, assignee_ids: [ user.id ]
rescue ActiveRecord::RecordNotUnique
+1 -1
View File
@@ -7,7 +7,7 @@ module Bubble::Commentable
def comment_created(comment)
increment! :comments_count
set_watching comment.creator, true
watch_by comment.creator
track_event :commented, comment_id: comment.id
rescore
+6 -6
View File
@@ -9,11 +9,11 @@ module Bubble::Pinnable
pins.exists?(user: user)
end
def set_pinned(user, pinned)
if pinned
pins.find_or_create_by!(user: user)
else
pins.find_by(user: user)&.destroy
end
def pin_by(user)
pins.find_or_create_by!(user: user)
end
def unpin_by(user)
pins.find_by(user: user).tap { it.destroy }
end
end
+10 -13
View File
@@ -8,22 +8,19 @@ module Bubble::Taggable
scope :tagged_with, ->(tags) { joins(:taggings).where(taggings: { tag: tags }) }
end
def toggle_tag(tag)
tagged_with?(tag) ? untag(tag) : tag(tag)
def toggle_tag_with(title)
tag = bucket.account.tags.find_or_create_by!(title: title)
transaction do
if tagged_with?(tag)
taggings.destroy_by tag: tag
else
taggings.create tag: tag
end
end
end
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
+7 -8
View File
@@ -5,15 +5,19 @@ module Bubble::Watchable
has_many :watches, dependent: :destroy
has_many :watchers, -> { merge(Watch.watching) }, through: :watches, source: :user
after_create :set_watching_for_creator
after_create -> { watch_by creator }
end
def watched_by?(user)
watchers_and_subscribers(include_only_watching: true).include?(user)
end
def set_watching(user, watching)
watches.where(user: user).first_or_create.update!(watching: watching)
def watch_by(user)
watches.where(user: user).first_or_create.update!(watching: true)
end
def unwatch_by(user)
watches.where(user: user).first_or_create.update!(watching: false)
end
def watchers_and_subscribers(include_only_watching: false)
@@ -23,9 +27,4 @@ module Bubble::Watchable
User.where(id: subscribers.pluck(:id) +
watches.watching.pluck(:user_id) - watches.not_watching.pluck(:user_id))
end
private
def set_watching_for_creator
set_watching(creator, true)
end
end
+1 -1
View File
@@ -12,7 +12,7 @@ class Bucket < ApplicationRecord
after_save :update_bubbles_workflow, if: :saved_change_to_workflow_id?
scope :alphabetically, -> { order(name: :asc) }
scope :alphabetically, -> { order("lower(name)") }
private
def update_bubbles_workflow
+1 -1
View File
@@ -10,6 +10,6 @@ module Event::Particulars
end
def comment
@comment ||= Comment.find_by(id: comment_id)
@comment ||= account.comments.find_by(id: comment_id)
end
end
-6
View File
@@ -3,10 +3,4 @@ class Pin < ApplicationRecord
belongs_to :user
scope :ordered, -> { order(created_at: :desc) }
after_create_commit -> { broadcast_prepend_later_to [ user, :pins ],
target: "pins",
partial: "bubbles/pins/pin"
}
after_destroy_commit -> { broadcast_remove_to [ user, :pins ] }
end
+3
View File
@@ -7,6 +7,9 @@ class Tag < ApplicationRecord
has_many :bubbles, through: :taggings
validates :title, format: { without: /\A#/ }
normalizes :title, with: -> { it.downcase }
scope :alphabetically, -> { order("lower(title)") }
def hashtag
"#" + title
+5 -26
View File
@@ -1,36 +1,23 @@
class User < ApplicationRecord
include Avatar, Role, Transferable
include Accessor, Assignee, Avatar, Role, Transferable
belongs_to :account
has_many :sessions, dependent: :destroy
has_secure_password validations: false
has_many :accesses, dependent: :destroy
has_many :buckets, through: :accesses
has_many :accessible_bubbles, through: :buckets, source: :bubbles
has_many :filters, foreign_key: :creator_id, inverse_of: :creator, dependent: :destroy
has_many :pops, dependent: :nullify
has_many :assignments, foreign_key: :assignee_id, dependent: :destroy
has_many :assignings, foreign_key: :assigner_id, class_name: "Assignment"
has_many :assigned_bubbles, through: :assignments, source: :bubble
has_many :comments, inverse_of: :creator, dependent: :destroy
has_many :notifications, dependent: :destroy
has_one_attached :avatar
has_many :filters, foreign_key: :creator_id, inverse_of: :creator, dependent: :destroy
has_many :pops, dependent: :nullify
has_many :pins, dependent: :destroy
has_many :pinned_bubbles, through: :pins, source: :bubble
normalizes :email_address, with: ->(value) { value.strip.downcase }
after_create_commit :grant_access_to_buckets
scope :alphabetically, -> { order("lower(name)") }
scope :sorted_with_user_first, ->(user) { order(Arel.sql("users.id != ?, lower(name)", user.id)) }
def initials
name.to_s.scan(/\b\p{L}/).join.upcase
@@ -38,22 +25,14 @@ class User < ApplicationRecord
def deactivate
transaction do
sessions.destroy_all
sessions.delete_all
accesses.destroy_all
update! active: false, email_address: deactived_email_address
end
end
def current?
Current.user == self
end
private
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
+16
View File
@@ -0,0 +1,16 @@
module User::Accessor
extend ActiveSupport::Concern
included do
has_many :accesses, dependent: :destroy
has_many :buckets, through: :accesses
has_many :accessible_bubbles, through: :buckets, source: :bubbles
after_create_commit :grant_access_to_buckets
end
private
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
+9
View File
@@ -0,0 +1,9 @@
module User::Assignee
extend ActiveSupport::Concern
included do
has_many :assignments, foreign_key: :assignee_id, dependent: :destroy
has_many :assignings, foreign_key: :assigner_id, class_name: "Assignment"
has_many :assigned_bubbles, through: :assignments, source: :bubble
end
end
+1 -1
View File
@@ -1,5 +1,5 @@
<div class="flex align-center gap-half pad-inline">
<%= avatar_tag user, loading: :lazy, class: "flex-item-no-shrink" %>
<%= avatar_tag user, class: "flex-item-no-shrink" %>
<strong class="overflow-ellipsis">
<%= link_to user.name, user_path(user), class: "txt-ink btn btn--plain" %>
-23
View File
@@ -1,23 +0,0 @@
<% cache [ bubble, bubble.bucket ] do %>
<%= turbo_frame_tag bubble, :assignment do %>
<dialog class="popup panel flex-column align-start gap-half fill-white shadow" data-dialog-target="dialog">
<strong class="popup__title margin-block-start-half pad-inline-half txt-nowrap">Assign this to…</strong>
<%= form_with url: bucket_bubble_assignment_toggles_path(bubble.bucket, bubble), class: "flex flex-column full-width popup__list", data: { controller: "form" } do |form| %>
<% bubble.bucket.users.active.sort_by(&:name).each do |user| %>
<div class="btn popup__item">
<%= form.check_box "assignee_id[]", {
checked: bubble.assigned_to?(user),
data: { action: "change->form#submit" },
id: dom_id(user, :assign),
include_hidden: false,
}, user.id %>
<%= form.label "assignee_id[]", user.name, for: dom_id(user, :assign), class: "overflow-ellipsis" %>
<%= icon_tag "check", size: 18, class: "checked flex-item-justify-end" %>
</div>
<% end %>
<% end %>
</dialog>
<% end %>
<% end %>
-48
View File
@@ -1,48 +0,0 @@
<% cache bubble do %>
<%= tag.div class: [ "bubble", drafted: bubble.drafted?, popped: bubble.popped? ],
style: "view-transition-name: bubble-#{bubble.id}; --bubble-color: #{bubble.color}; #{bubble_rotation(bubble)}",
role: "group", aria: { label: bubble_title(bubble) },
data: {
bubble_size_target: "bubble",
activity_score: bubble.activity_score,
activity_score_at: bubble.activity_score_at.to_i,
controller: "animation upload-preview",
animation_play_class: "bubble--wobble",
animation_play_on_load_value: true,
action: "mouseover->animation#play" } do %>
<div>
<h1 class="bubble__title" role="presentation">
<% if bubble.published? %>
<%= turbo_frame_tag bubble, :edit do %>
<%= link_to bubble_title(bubble), edit_bucket_bubble_path(bubble.bucket, bubble),
class: "txt-undecorated bubble__title-rendered",
role: "button", aria: { label: "Edit name: #{bubble_title(bubble)}" } %>
<% end %>
<% else %>
<%= form_with model: bubble, url: bucket_bubble_path(bubble.bucket, bubble), id: "bubble_form", data: { controller: "auto-save" } do |form| %>
<%= form.text_area :title, placeholder: "Name it…", class: "input full-width borderless txt-align-start card__title-field", autofocus: bubble.title.blank?, data: { action: "auto-save#change blur->auto-save#submit keydown.enter->auto-save#submit:prevent keydown.ctrl+enter->auto-save#submit:prevent" } %>
<% end %>
<% end %>
<div class="bubble__tags flex flex-wrap align-end justify-center gap-half txt-tight-lines flex-item-no-shrink pad-inline-double">
<%= render "bubbles/tags", bubble: bubble %>
</div>
</h1>
</div>
<div class="bubble__shape"></div>
<svg class="bubble__splat" viewBox="0 0 934 914" xmlns="http://www.w3.org/2000/svg"><circle cx="485.2" cy="467.2" r="286.5"/><path d="m197.7 219.1c-.2-7.1 5.8-11.7 12.7-13.2 34-7.1 53.5 29.2 81.4 40 39.5 6.4 25.9-54 30.8-77.7 17.2-65.3 78.4 67.2 108.2 55.7 26.8-17.3 25.9-58.8 38.7-86.1 9.6-41.5 30.4-2.6 48.6 10.1 14.2 11.2 31.9 17.9 45.1 29.9 13 11.2 20 29.1 35.8 36.8 20.2 9.1 44.8 2.3 65.2 11.8 18.3 7.2 33.8 22.6 52.5 30.4 23 8.6 42.4 1.8 37.8 35.1 2.7 36.8 38.3 71.9 40.1 108.6-.2 21.1-30.1 21.7-22.4 36.3 22.1 32.5-7.7 51.1 21 93.3 7 43.2-91.2-7.2-14.3 78.2 13.2 13.7 22.3 28.2 4.5 40.2-18.7 18.4-19.5 18.4-45 13.8-15.5-1.2 1.8 16.5-4.6 22.7-21.1 8-6.7 22-14.6 34.9-6 3.2-14.7-2.6-21.1-4-17.2-6.2-11.5 17.5-15.3 25.7-4 7.3-15.6 1.5-22.2-.2-26.7-10.8-55.1-3.1-82.4-.2-14.2 1.1-26 2.2-30.2 18.3-8 20.8-7.5 87.5-41.5 68.5-15.3-7.2-29.8-51.7-45.3-26.4-2.8 3.9-6.4 9.3-11.2 5.9-20.7-27.6-17-71-54.3-25.6-29.6 29.4-32.5 2.5-48.8-20.5-18.9-17.1-48.4-20.5-61.6-44.6-18.5-27.6-35.8-54.6-60.2-77.9-10.5-10.8-60.9-17.2-34.7-35.6 42.5-23.5-50.6-14-19.6-30.8 33.5-11.7 12.7-23.2-10.3-28.1-6.2-1.9-22-3.3-22.7-8.2 2.9-11.2 13.8-26.5 22.6-35.7 38.3-20.1-7-33.5-23.6-48.2-4-4.4 4.8-2.9 5.5-5.8-7.4-11.8-11.8-10-.4-21.8 3.2-7.8-4.9-17.6-3.8-25.5 8.1-21 57.9-6.5 64.2-36.2-3.5-26.1-16.4-50.3 3.5-75 5.9-8.8 22.4-16.8 19.3-28.7-5.7-14.7-26.4-23.7-27.6-40.2z"/><path d="m573 256.7s29.6-24.9 55-64.4 40.3-158 88.1-127.9c44.4 28-27.7 97.3-60.3 143.3-32.6 45.9-14.3 85.7-14.3 85.7l-68.5-36.6z"/><path d="m606.4 674.7s-11.3 37-11.2 84c0 47 51.9 154.6-4.6 155.3-52.5.6-29.6-96.8-27.1-153.1s-34.5-79.7-34.5-79.7z"/><path d="m325.5 622.7s-34 16.1-71.9 44c-37.8 27.9-104.9 118.2-126.8 89.7-20.3-26.5 66.8-72.5 113.1-104.6s51.2-64.9 51.2-64.9l34.4 35.9z"/><path d="m225.7 388.5s-30.8-35.1-75.8-72.7c-45.1-37.5-169.2-98.3-146.9-126.5 20.8-26.1 104.8 63 157.6 109.3 52.9 46.2 90.4 46.9 90.4 46.9l-25.4 43z"/><path d="m327.7 593.7s-47.6 1.4-103.9 17.7c-56.3 16.2-170.6 104.9-187.6 48.8-15.8-52.2 107.7-62.8 175.9-79.7s85.8-61.9 85.8-61.9l29.9 75.1z"/><path d="m443.5 344.2s0-46.7-8.9-104.6c-9-57.9-62-185.6-26.7-192 32.9-5.9 37 116.5 46.2 186.2 9.2 69.6 36.9 95 36.9 95l-47.5 15.5z"/><path d="m433.2 340.9s-6.8-37-19.5-82.8c-12.8-45.8-56.9-146.5-40.8-151.7 15-4.8 34.8 92 49.3 147s31.6 74.9 31.6 74.9l-20.6 12.5z"/><path d="m496.8 298.1s10.8-45.7 13.6-104.2c2.8-58.6-29.9-194.6 12.9-193.4 39.8 1.1 16.5 121.7 11.2 191.8-5.3 70 21.3 100.6 21.3 100.6l-59.1 5.2z"/><path d="m647.8 525s22.1 41.1 57.6 87.8c35.4 46.7 142.8 133.8 114.7 156.2-26.1 20.8-88-84.9-129.1-141.8-41.2-56.9-77.6-66-77.6-66z"/><path d="m658.4 523s23.5 29.3 56.5 63.5 119.7 101.9 108 114.1c-10.9 11.4-74.3-64.4-113.3-105.9s-63.4-50.9-63.4-50.9z"/><path d="m622.8 591s12.2 45.3 37.6 98.2 118.8 157 80.6 176.3c-35.6 17.9-72.4-99.3-101.1-163.4-28.6-64.1-66.6-78.4-66.6-78.4l49.5-32.6z"/><path d="m613.7 441.6s43.3 17.5 100.4 30.7c57.1 13.3 195.4 11.6 188.1 46.8-6.8 32.7-121.9-9.1-189.9-26.6s-101.9-1.2-101.9-1.2l3.3-49.8z"/><path d="m666.2 496.5s38.4 27.1 91.7 51.5 191.7 44.8 174.6 84.1c-15.9 36.5-119.1-30.1-182.2-61.1-63-31-101.3-17.7-101.3-17.7l17.2-56.7z"/><path d="m57.2 500.1c-7.1-1.3-14.4-2-21.5-.7-13.9 2.4-27.3 11.4-31.7 25.2-5 15.2 2.5 30.2 12.9 41.7 7.5 8.5 16.3 16.5 27.6 16.6 19.9-.7 36.3-19.9 37.6-39 1.6-18.3-8.2-36.2-27.2-40.2-3.5-.8-7.2-1.2-10.8-1.5"/><path d="m796.1 362.9c5.4 21.4 22.7 41 44.2 46.3 16.7 4.5 33.7-3 43.2-17.2 24.3-38.2-3-125.5-53.2-116.5-8 1.5-15.8 5.8-21.4 11.6-18.2 19.3-19.3 51.3-12.8 75.6z"/><path d="m255.1 782.9c5.4 21.4 22.7 41 44.2 46.3 16.7 4.5 33.7-3 43.2-17.2 24.3-38.2-3-125.5-53.2-116.5-8 1.5-15.8 5.8-21.4 11.6-18.2 19.3-19.3 51.3-12.8 75.6z"/><path d="m771 224.4c-3.4-.5-6.8-1-10.2-1.1-23.8-1.1-36.1 23.5-16.7 36.4 9.4 6.3 22.2 2.4 26.7-7.7 5.2-11.6-.7-20.3-10.6-26.9"/><path d="m402 812.4c-3.4-.5-6.8-1-10.2-1.1-23.8-1.1-36.1 23.5-16.7 36.4 9.4 6.3 22.2 2.4 26.7-7.7 5.2-11.6-.7-20.3-10.6-26.9"/></svg>
<h2 contents>
<%= link_to bucket_bubble_path(bubble.bucket, bubble), class: "bubble__link" do %>
<span class="for-screen-reader"><%= bubble_title(bubble) %></span>
<% end %>
</h2>
<%= render "bubbles/assignments", bubble: bubble %>
<%= render "bubbles/boosts", bubble: bubble %>
<%= render "bubbles/comments", bubble: bubble %>
<%= render "bubbles/date", bubble: bubble %>
<%= render "bubbles/image", bubble: bubble %>
<% end %>
<% end %>
+1 -1
View File
@@ -1,5 +1,5 @@
<% if bubble.image.attached? %>
<%= button_to bucket_bubble_image_path(bubble.bucket, bubble), method: :delete, class: "btn" do %>
<%= button_to bubble_image_path(bubble), method: :delete, class: "btn" do %>
<%= icon_tag "picture-remove" %>
<span class="for-screen-reader">Clear background image</span>
<% end %>
+10 -5
View File
@@ -1,13 +1,18 @@
<%= messages_tag(bubble) do %>
<%# Template Dependency: comments/comment %>
<%# Template Dependency: event_summaries/event_summary %>
<%= render bubble.messages, cached: true %>
<%= render partial: "messages/message", collection: bubble.messages, cached: true %>
<%= render "comments/new", bubble: bubble, cached: true %>
<div class="comments__subscribers flex flex-column margin-block txt-align-start full-width">
<strong class="txt-uppercase">Subscribers</strong>
<p class="margin-none-block-start margin-block-end-half"><%= pluralize(@bubble.watchers.count, "person") %> will be notified when someone comments on this.</p>
<p class="margin-none-block-start margin-block-end-half">
<%= pluralize(@bubble.watchers.count, "person") %> will be notified when someone comments on this.
</p>
<div class="flex align-center flex-wrap gap-half max-width txt-normal">
<%= render partial: "bubbles/watches/watcher", collection: @bubble.watchers.sorted_with_user_first(Current.user) %>
<% prepend_current_user_to(@bubble.watchers.alphabetically).each do |watcher| %>
<%= avatar_tag watcher %>
<% end %>
</div>
</div>
<% end %>
+2 -9
View File
@@ -1,7 +1,7 @@
<div class="card__actions-container center border-radius flex-inline align-center justify-center gap" style="--bubble-color: <%= @bubble.color %>">
<% if bubble.popped? %>
<span class="card__pop-message">Completed by <%= bubble.popped_by.name %> on <%= local_datetime_tag(bubble.popped_at, style: :shortdate) %>.</span>
<%= button_to bucket_bubble_pop_path(bubble.bucket, bubble), method: :delete, class: "btn btn--plain borderless fill-transparent" do %>
<%= button_to bubble_pop_path(bubble), method: :delete, class: "btn btn--plain borderless fill-transparent" do %>
<span>Un-do</span>
<% end %>
<% else %>
@@ -16,17 +16,10 @@
<strong class="popup__title margin-block-start-half pad-inline-half">Close as…</strong>
<% Current.account.pop_reasons.labels.each do |label| %>
<%= button_to bucket_bubble_pop_path(bubble.bucket, bubble, reason: label), class: "btn popup__item full-width", form_class: "full-width" do %>
<%= button_to bubble_pop_path(bubble, reason: label), class: "btn popup__item full-width", form_class: "full-width" do %>
<span class="overflow-ellipsis"><%= label %></span>
<% end %>
<% end %>
<% if bubble.doing? %>
<hr class="separator--horizontal full-width flex-item-grow" style="--border-color: var(--color-subtle)" aria-hidden="true">
<%= button_to bucket_bubble_engagement_path(bubble.bucket, bubble), method: :delete, class: "btn popup__item full-width" do %>
<span class="overflow-ellipsis">Move back to Considering</span>
<% end %>
<% end %>
</dialog>
</div>
+1 -1
View File
@@ -1,3 +1,3 @@
<%= button_to bucket_bubble_publish_path(bubble.bucket, bubble), class: "btn txt-small btn--link", style: "--btn-background: #{ bubble.color }" do %>
<%= button_to bubble_publish_path(bubble), class: "btn txt-small btn--link", style: "--btn-background: #{bubble.color}" do %>
<span>Post to project</span>
<% end %>
-31
View File
@@ -1,31 +0,0 @@
<% cache [ bubble, Current.account ] do %>
<%= turbo_frame_tag bubble, :tagging do %>
<dialog class="popup panel flex-column align-start justify-start fill-white shadow" data-dialog-target="dialog">
<strong class="popup__title margin-block-half pad-inline-half">Tag this…</strong>
<%= form_with url: bucket_bubble_tagging_toggles_path(bubble.bucket, bubble), class: "flex gap-half align-center full-width pad-inline-half margin-block-end" do |form| %>
<%= form.text_field :tag_title, placeholder: "New tag name...", class: "input txt-small full-width" %>
<%= form.button "Add a tag…", type: "submit", class: "btn txt-small" do %>
<%= icon_tag "add" %>
<span class="for-screen-reader">Add a tag</span>
<% end %>
<% end %>
<%= form_with url: bucket_bubble_tagging_toggles_path(bubble.bucket, bubble), class: "flex flex-column full-width popup__list", data: { controller: "form" } do |form| %>
<% tags.sort_by { |tag| tag.hashtag.downcase }.each do |tag| %>
<div class="btn popup__item">
<%= form.check_box "tag_id[]", {
checked: bubble.tagged_with?(tag),
data: { action: "change->form#submit" },
id: dom_id(tag, :tag),
include_hidden: false,
}, tag.id %>
<%= form.label "tag_id[]", tag.hashtag, for: dom_id(tag, :tag), class: "overflow-ellipsis" %>
<%= icon_tag "check", size: 18, class: "checked flex-item-justify-end" %>
</div>
<% end %>
<% end %>
</dialog>
<% end %>
<% end %>
@@ -0,0 +1 @@
<%= turbo_stream.replace([ @bubble, :assignees ], partial: "bubbles/cards/perma/assignees", locals: { bubble: @bubble }) %>
@@ -0,0 +1,14 @@
<%= turbo_frame_tag @bubble, :assignment do %>
<dialog class="popup panel flex-column align-start gap-half fill-white shadow" data-dialog-target="dialog">
<strong class="popup__title margin-block-start-half pad-inline-half txt-nowrap">Assign this to…</strong>
<div class="flex flex-column full-width popup__list">
<% @bubble.bucket.users.active.alphabetically.each do |user| %>
<div class="btn popup__item">
<%= button_to user.name, bubble_assignments_path(@bubble, params: { assignee_id: user.id }), method: :post %>
<%= icon_tag "check", size: 18, class: "checked flex-item-justify-end" if @bubble.assignees.include?(user) %>
</div>
<% end %>
</div>
</dialog>
<% end %>
@@ -1,10 +1,11 @@
<%= form_with url: bucket_bubble_boosts_path(bubble.bucket, bubble), class: "pad-inline flex align-center gap" do %>
<div class="pad-inline flex align-center gap">
<span class="overflow-ellipsis">
<strong><%= text_field_tag :boost_count, bubble.boosts_count, min: 1, class: "boost__input input", autocomplete: "off" %></strong>
<span><%= bubble.boosts_count == 1 ? "boost" : "boosts" %></span>
</span>
<%= tag.button class: "btn", type: :submit, style: "font-size: 0.5em" do %>
<%= button_to bubble_boosts_path(bubble), class: "btn", type: :submit, style: "font-size: 0.5em" do %>
<%= icon_tag "add" %>
<span class="for-screen-reader">Boost</span>
<% end %>
<% end %>
</div>
@@ -1,5 +1,5 @@
<%= turbo_stream.update dom_id(@bubble, :boosts) do %>
<%= render "boosts/boosts", bubble: @bubble %>
<%= render "bubbles/boosts/boosts", bubble: @bubble %>
<% end %>
<%= turbo_stream.replace dom_id(@bubble, :messages) do %>
+4 -2
View File
@@ -1,13 +1,15 @@
<div class="card card--mini shadow border flex gap position-relative txt-align-start full-width border-radius" style="view-transition-name: <%= dom_id(bubble, :ticket) %>;">
<%= avatar_image_tag bubble.creator, loading: :lazy, class: "avatar" %>
<%= avatar_image_tag bubble.creator, class: "avatar" %>
<div class="card__body flex flex-column gap-half full-width flex-item-grow min-width max-width">
<h3 class="card__title">
<%= bubble_title(bubble) %>
</h3>
<div class="txt-small overflow-ellipsis txt-uppercase"><%= "Added to #{bubble.bucket.name} by #{bubble.creator.name}" %>
<%= local_datetime_tag(bubble.created_at, style: :daysAgo) %>
<%= local_datetime_tag(bubble.created_at, style: :daysago) %>
</div>
<div class="card__stuff flex align-center gap">
<%= render "bubbles/cards/mini/assignees", bubble: bubble %>
<div class="overflow-ellipsis"><%= render "bubbles/cards/mini/tags", bubble: bubble %></div>
+4 -11
View File
@@ -1,7 +1,7 @@
<% cache [bubble] do %>
<article class="card shadow border flex flex-column position-relative txt-align-start border-radius"
style="--bubble-color: <%= bubble.color %>; view-transition-name: <%= dom_id(bubble, :ticket) %>;"
id="<%= dom_id(bubble, :ticket) %>">
<% cache bubble do %>
<article class="card shadow border flex flex-column position-relative txt-align-start full-width border-radius"
style="--bubble-color: <%= bubble.color %>; view-transition-name: <%= dom_id(bubble, :ticket) %>;"
id="<%= dom_id(bubble, :ticket) %>">
<header class="card__header flex align-center gap min-width">
<%= link_to bubble.bucket.name, bubbles_path(bucket_ids: [bubble.bucket]),
class: "card__bucket txt-uppercase overflow-ellipsis txt-reversed" %>
@@ -32,13 +32,6 @@
<%= render "bubbles/stagings/stages", bubble: bubble %>
<% end %>
<% end %>
<% if bubble.considering? %>
<%= button_to bucket_bubble_engagement_path(bubble.bucket, bubble), method: :post, class: "card__move-button card__hide-on-index btn btn--reversed" do %>
<span class="txt-nowrap">Move to Doing</span>
<%= icon_tag "assigned" %>
<% end %>
<% end %>
</div>
<footer class="card__footer full-width flex align-start gap">
@@ -1,16 +1,14 @@
<%= turbo_frame_tag bubble, :assignees do %>
<div class="flex">
<% bubble.assignees.each do |assignee| %>
<%= avatar_tag assignee, loading: :lazy, class: "avatar" %>
<% end %>
<div class="flex" id="<%= dom_id(bubble, :assignees) %>">
<% bubble.assignees.each do |assignee| %>
<%= avatar_tag assignee, class: "avatar" %>
<% end %>
<div class="position-relative" data-controller="dialog" data-action="keydown.esc->dialog#close click@document->dialog#closeOnClickOutside">
<button class="btn card__hide-on-index <%= bubble.creating? || bubble.drafted? ? "fill-highlight" : "fill-transparent" %>" data-action="click->dialog#open:stop">
<%= icon_tag "add" %>
<span class="for-screen-reader">Assign</span>
</button>
<div class="position-relative" data-controller="dialog" data-action="keydown.esc->dialog#close click@document->dialog#closeOnClickOutside">
<button class="btn card__hide-on-index <%= bubble.creating? || bubble.drafted? ? "fill-highlight" : "fill-transparent" %>" data-action="click->dialog#open:stop">
<%= icon_tag "add" %>
<span class="for-screen-reader">Assign</span>
</button>
<%= yield %>
</div>
<%= yield %>
</div>
<% end %>
</div>
@@ -1,18 +1,18 @@
<div class="card__meta flex txt-tight-lines txt-uppercase">
<div class="flex flex-column gap txt-align-end">
<span class="pad-inline overflow-ellipsis"><%= bubble.creating? ? "Added" : "Updated" %>
<%= local_datetime_tag(bubble.updated_at, style: :daysAgo) %>
<%= local_datetime_tag(bubble.updated_at, style: :daysago) %>
</span>
<hr class="full-width separator--horizontal unpad margin-none">
<% if bubble.creating? %>
<span class="pad-inline overflow-ellipsis">Expires
<%= local_datetime_tag(bubble.auto_pop_at, style: :daysAgo) %>
<%= local_datetime_tag(bubble.auto_pop_at, style: :indays) %>
</span>
<% else %>
<span class="pad-inline overflow-ellipsis">Added
<%= local_datetime_tag(bubble.created_at, style: :daysAgo) %>
<%= local_datetime_tag(bubble.created_at, style: :daysago) %>
</span>
<% end %>
</div>
@@ -1,7 +1,7 @@
<div class="card__people flex gap margin-inline-start">
<div class="flex flex-column align-center">
<span class="txt-uppercase">Reported</span>
<%= avatar_tag bubble.creator, loading: :lazy, class: "avatar" %>
<%= avatar_tag bubble.creator, class: "avatar" %>
</div>
<div class="card__assignees flex flex-column align-center">
@@ -1,7 +1,7 @@
<%= turbo_frame_tag bubble, :assignees do %>
<div class="flex">
<% bubble.assignees.each do |assignee| %>
<%= avatar_tag assignee, loading: :lazy, class: "avatar" %>
<%= avatar_tag assignee, class: "avatar" %>
<% end %>
<div class="position-relative" data-controller="dialog" data-action="keydown.esc->dialog#close click@document->dialog#closeOnClickOutside">
@@ -1,3 +1,3 @@
<%= render "bubbles/cards/common/assignees", bubble: bubble do %>
<%= turbo_frame_tag bubble, :assignment, src: new_bucket_bubble_assignment_toggle_path(bubble.bucket, bubble) %>
<%= turbo_frame_tag bubble, :assignment, src: new_bubble_assignment_path(bubble) %>
<% end %>
@@ -1,3 +1,3 @@
<%= turbo_frame_tag dom_id(bubble, :boosts) do %>
<%= render "boosts/boosts", bubble: bubble %>
<%= render "bubbles/boosts/boosts", bubble: bubble %>
<% end %>
+20 -22
View File
@@ -1,24 +1,22 @@
<%= turbo_frame_tag bubble, :tags do %>
<div class="card__tags flex align-center flex-item-grow min-width">
<div class="overflow-ellipsis">
<% if bubble.tags.any? || bubble.creating? %>
<span># </span>
<% bubble.tags.each_with_index do |tag, index| %>
<%= link_to bubbles_path(bucket_ids: [ bubble.bucket ], tag_ids: [ tag.id ]),
class: "card__tag btn btn--plain min-width txt-uppercase fill-transparent", data: { turbo_frame: "_top" } do %>
<span class="overflow-ellipsis"><%= tag.title %></span>
<% end %><%= ", " unless index == bubble.tags.size - 1 %>
<% end %>
<div id="<%= dom_id(bubble, :tags) %>" class="card__tags flex align-center flex-item-grow min-width">
<div class="overflow-ellipsis">
<% if bubble.tags.any? || bubble.creating? %>
<span># </span>
<% bubble.tags.each_with_index do |tag, index| %>
<%= link_to bubbles_path(bucket_ids: [ bubble.bucket ], tag_ids: [ tag.id ]),
class: "card__tag btn btn--plain min-width txt-uppercase fill-transparent" do %>
<span class="overflow-ellipsis"><%= tag.title %></span>
<% end %><%= ", " unless index == bubble.tags.size - 1 %>
<% end %>
<% end %>
</div>
<div class="position-relative" data-controller="dialog" data-action="keydown.esc->dialog#close click@document->dialog#closeOnClickOutside">
<button class="tag-picker__button btn card__hide-on-index <%= bubble.creating? || bubble.drafted? ? "fill-highlight" : "fill-transparent" %>" data-action="click->dialog#open:stop">
<%= icon_tag "tag" %>
<span class="for-screen-reader">Add a tag</span>
</button>
<%= turbo_frame_tag bubble, :tagging, src: new_bucket_bubble_tagging_toggle_path(bubble.bucket, bubble) %>
</div>
</div>
<% end %>
<div class="position-relative" data-controller="dialog" data-action="keydown.esc->dialog#close click@document->dialog#closeOnClickOutside">
<button class="tag-picker__button btn card__hide-on-index <%= bubble.creating? || bubble.drafted? ? "fill-highlight" : "fill-transparent" %>" data-action="click->dialog#open:stop">
<%= icon_tag "tag" %>
<span class="for-screen-reader">Add a tag</span>
</button>
<%= turbo_frame_tag bubble, :tagging, src: new_bubble_tagging_path(bubble) %>
</div>
</div>
+8 -8
View File
@@ -1,13 +1,13 @@
<%= turbo_frame_tag dom_id(@bubble, :pin) do %>
<% if @bubble.pinned_by? Current.user %>
<%= button_to bucket_bubble_pin_path(@bubble.bucket, @bubble), method: :delete, class: "btn btn--reversed" do %>
<%= icon_tag "pinned" %>
<span class="for-screen-reader">Un-pin this card</span>
<% end %>
<%= button_to bubble_pin_path(@bubble), method: :delete, class: "btn btn--reversed" do %>
<%= icon_tag "pinned" %>
<span class="for-screen-reader">Un-pin this card</span>
<% end %>
<% else %>
<%= button_to bucket_bubble_pin_path(@bubble.bucket, @bubble), class: "btn" do %>
<%= icon_tag "unpinned" %>
<span class="for-screen-reader">Pin this card</span>
<% end %>
<%= button_to bubble_pin_path(@bubble), class: "btn" do %>
<%= icon_tag "unpinned" %>
<span class="for-screen-reader">Pin this card</span>
<% end %>
<% end %>
<% end %>
@@ -1,3 +0,0 @@
<%= turbo_frame_tag "pins" do %>
<%= render collection: @pins, partial: "bubbles/pins/pin" %>
<% end %>
@@ -1,3 +0,0 @@
<%= turbo_frame_tag "pins" do %>
<%= render collection: @pins, partial: "bubbles/pins/pin" %>
<% end %>
+36 -11
View File
@@ -5,7 +5,7 @@
<div class="fill-selected position-sticky flex align-center gap-half justify-center border-block margin-block-end"
style="view-transition-name: draft-banner; --bubble-color: <%= @bubble.color %>;">
You have an unsaved work. Would you like to continue where you left off?
<%= button_to bucket_bubble_recover_path(@bubble.bucket, @bubble), class: "btn btn--reversed", data: { turbo_action: "replace" } do %>
<%= button_to bubble_recover_path(@bubble), class: "btn btn--reversed", data: { turbo_action: "replace" } do %>
<span>Restore</span>
<% end %>
</div>
@@ -22,21 +22,46 @@
<nav class="align-start max-width">
<%= link_to_home %>
<%= link_to "#", class: "btn flex-item-justify-start", data: { controller: "back-navigation hotkey", action: "keydown.esc@document->hotkey#click", back_navigation_fallback_destination_value: bubbles_path(bucket_id: @bubble.bucket) } do %>
<%= icon_tag "arrow-left" %>
<span class="for-screen-reader">Go Back</span>
<% end %>
<%= link_to_back fallback_path: bubbles_path(bucket_id: @bubble.bucket) %>
<div class="card__actions-container card__actions-container--top center border-radius flex-inline align-center justify-center gap" style="--bubble-color: <%= @bubble.color %>;">
<span>Considering</span>
<% if @bubble.doing? %>
<%= form_with url: bubble_engagement_path(@bubble), method: :delete, data: { controller: "form" }, class: "flex" do %>
<label class="switch flex-item-no-shrink">
<input type="checkbox" class="switch__input" data-action="form#submit" checked>
<span class="switch__btn round"></span>
<span class="for-screen-reader">Switch to Considering</span>
</label>
<% end %>
<% else %>
<%= form_with url: bubble_engagement_path(@bubble), method: :post, data: { controller: "form" }, class: "flex" do %>
<label class="switch flex-item-no-shrink">
<input type="checkbox" class="switch__input" data-action="form#submit">
<span class="switch__btn round"></span>
<span class="for-screen-reader">Switch to Doing</span>
</label>
<% end %>
<% end %>
<span>Doing</span>
</div>
<div class="btn btn--placeholder flex-item-justify-end"></div>
<div class="btn btn--placeholder"></div>
</nav>
<% end %>
<div data-controller="beacon" data-beacon-url-value="<%= bucket_bubble_readings_url(@bubble.bucket, @bubble) %>">
<div class="card__container position-relative <%= "card__container--pointing" unless @bubble.published? %>">
<div data-controller="beacon" data-beacon-url-value="<%= bubble_reading_url(@bubble) %>">
<div class="card__container position-relative <%= "card__container--pointing" unless @bubble.published? %>" style="--bubble-color: <%= @bubble.color %>;">
<%= render "bubbles/cards/perma", bubble: @bubble %>
<div class="card__actions flex flex-column gap-half">
<button class="car__star-button btn">
<%= icon_tag "star" %>
<span class="for-screen-reader">Star this card</span>
<input type="checkbox" class="card__star-input">
</button>
<%= render "bubbles/color", bubble: @bubble %>
<%= render "bubbles/image", bubble: @bubble %>
<%= button_to bucket_bubble_path(@bubble.bucket, @bubble),
@@ -51,8 +76,8 @@
<% if @bubble.published? %>
<div class="card__actions card__actions--right flex flex-column gap-half">
<%= turbo_frame_tag dom_id(@bubble, :watch), src: bucket_bubble_watch_path(@bubble.bucket, @bubble), refresh: :morph %>
<%= turbo_frame_tag dom_id(@bubble, :pin), src: bucket_bubble_pin_path(@bubble.bucket, @bubble), refresh: :morph %>
<%= turbo_frame_tag dom_id(@bubble, :watch), src: bubble_watch_path(@bubble), refresh: :morph %>
<%= turbo_frame_tag dom_id(@bubble, :pin), src: bubble_pin_path(@bubble), refresh: :morph %>
</div>
<% end %>
</div>
@@ -69,7 +94,7 @@
<div class="comments pad-block">
<div class="comment comment--new flex-inline align-start full-width">
<figure class="comment__avatar flex-item-no-shrink" aria-hidden="true">
<%= avatar_tag Current.user, hidden_for_screen_reader: true, loading: :lazy %>
<%= avatar_tag Current.user, hidden_for_screen_reader: true %>
</figure>
<div class="comment__content flex-inline flex-column full-width">
@@ -86,7 +111,7 @@
<% if @bubble.creating? %>
<div class="margin-block-start flex align-center gap">
<%= button_to "Create card", bucket_bubble_publish_path(@bubble.bucket, @bubble), class: "btn btn--reversed" %>
<%= button_to "Create card", bubble_publish_path(@bubble), class: "btn btn--reversed" %>
<%= button_to "Save as draft", bucket_bubble_path(@bubble.bucket, @bubble), name: "bubble[status]", value: "drafted", method: :put, class: "btn" %>
</div>
<% end %>
@@ -0,0 +1 @@
<%= turbo_stream.replace([ @bubble, :tags ], partial: "bubbles/cards/perma/tags", locals: { bubble: @bubble }) %>
+22
View File
@@ -0,0 +1,22 @@
<%= turbo_frame_tag @bubble, :tagging do %>
<dialog class="popup panel flex-column align-start justify-start fill-white shadow" data-dialog-target="dialog">
<strong class="popup__title margin-block-half pad-inline-half">Tag this…</strong>
<%= form_with url: bubble_taggings_path(@bubble), class: "flex gap-half align-center full-width pad-inline-half margin-block-end" do |form| %>
<%= form.text_field :tag_title, placeholder: "New tag name...", class: "input txt-small full-width" %>
<%= form.button "Add a tag…", type: "submit", class: "btn txt-small" do %>
<%= icon_tag "add" %>
<span class="for-screen-reader">Add a tag</span>
<% end %>
<% end %>
<div class: "flex flex-column full-width popup__list">
<% @tags.each do |tag| %>
<div class="btn popup__item">
<%= button_to tag.hashtag, bubble_taggings_path(@bubble, params: { tag_title: tag.title }), method: :post %>
<%= icon_tag "check", size: 18, class: "checked flex-item-justify-end" if @bubble.tagged_with?(tag) %>
</div>
<% end %>
</div>
</dialog>
<% end %>
@@ -1,7 +1,4 @@
<div class="flex gap-half justify-space-between align-center" style="--btn-icon-size: 1rem">
<%= tag.hashtag %>
<% if bubble.tagged_with?(tag) %>
<%= icon_tag "check" %>
<% end %>
<%= icon_tag "check" if bubble.tagged_with?(tag) %>
</div>
@@ -1 +0,0 @@
<%= avatar_tag watcher, loading: :lazy %>
+2 -2
View File
@@ -1,11 +1,11 @@
<%= turbo_frame_tag dom_id(@bubble, :watch) do %>
<% if @bubble.watched_by? Current.user %>
<%= button_to bucket_bubble_watch_path(@bubble.bucket, @bubble), method: :delete, class: "btn btn--reversed" do %>
<%= button_to bubble_watch_path(@bubble), method: :delete, class: "btn btn--reversed" do %>
<%= icon_tag "bell" %>
<span class="for-screen-reader">Stop watching</span>
<% end %>
<% else %>
<%= button_to bucket_bubble_watch_path(@bubble.bucket, @bubble), class: "btn" do %>
<%= button_to bubble_watch_path(@bubble), class: "btn" do %>
<%= icon_tag "bell-off" %>
<span class="for-screen-reader">Watch this</span>
<% end %>
+1 -1
View File
@@ -1,6 +1,6 @@
<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 %>
<%= avatar_tag user %>
</figure>
<div class="min-width">
+1 -4
View File
@@ -2,10 +2,7 @@
<% content_for :header do %>
<nav>
<%= link_to bubbles_path(bucket_ids: [ @bbucket ]), class: "btn flex-item-justify-start", data: { controller: "hotkey", action: "keydown.esc@document->hotkey#click" } do %>
<%= icon_tag "arrow-left" %>
<span class="for-screen-reader">Go back</span>
<% end %>
<%= link_to_back fallback_path: bubbles_path(bucket_ids: [ @bbucket ]) %>
<%= form_with model: @bucket, class: "flex-item-justify-end", method: :delete do |form| %>
<%= form.button class: "btn btn--negative", data: { turbo_confirm: "Are you sure you want to delete this?" } do %>
+1 -1
View File
@@ -1,6 +1,6 @@
<%= comment_tag comment do %>
<figure class="comment__avatar flex-item-no-shrink" aria-hidden="true">
<%= avatar_tag comment.creator, hidden_for_screen_reader: true, loading: :lazy %>
<%= avatar_tag comment.creator, hidden_for_screen_reader: true %>
</figure>
<%= render "comments/body", comment: comment %>
+1 -1
View File
@@ -1,6 +1,6 @@
<div class="comment comment--new flex-inline align-start full-width">
<figure class="comment__avatar flex-item-no-shrink" aria-hidden="true">
<%= avatar_tag Current.user, hidden_for_screen_reader: true, loading: :lazy %>
<%= avatar_tag Current.user, hidden_for_screen_reader: true %>
</figure>
<div class="comment__content flex-inline flex-column full-width">
@@ -6,7 +6,7 @@
data-controller="reaction-delete" data-reaction-delete-perform-class="reaction--deleting"
data-reaction-delete-reveal-class="expanded" data-reaction-delete-reacter-id-value="<%= reaction.reacter.id %>">
<figure class="reaction__avatar margin-none flex-item-no-shrink">
<%= avatar_tag reaction.reacter, aria: { label: "#{reaction.reacter.name} reacted #{reaction.content}" }, loading: :lazy %>
<%= avatar_tag reaction.reacter, aria: { label: "#{reaction.reacter.name} reacted #{reaction.content}" } %>
</figure>
<%= tag.span reaction.content, role: "button",
+1 -1
View File
@@ -48,7 +48,7 @@
<% if Current.user %>
<%= render "notifications/tray" %>
<%= render "bubbles/pins/tray" %>
<%= render "my/pins/tray" %>
<%= turbo_frame_tag "terminal", src: terminal_path, data: { turbo_permanent: true } %>
<% end %>
</footer>
+5 -1
View File
@@ -1 +1,5 @@
<%= render message.messageable %>
<% cache message do %>
<%# Template Dependency: comments/comment %>
<%# Template Dependency: event_summaries/event_summary %>
<%= render partial: message.messageable.to_partial_path, object: message.messageable %>
<% end %>
@@ -2,7 +2,7 @@
<%= tag.dialog id: "pin-tray", class: "tray pin-tray",
data: { controller: "dialog", turbo_permanent: true, dialog_modal_value: false, dialog_target: "dialog", action: "keydown.esc->dialog#close:stop click@document->dialog#closeOnClickOutside" } do %>
<%= turbo_frame_tag "pins", src: bubbles_pins_tray_path %>
<%= turbo_frame_tag "pins", src: my_pins_path %>
<div class="tray__actions border border-radius">
<button class="txt-uppercase btn btn--reversed borderless fill-transparent txt-x-small translucent center" data-action="click->dialog#close">Pinned</button>
@@ -10,4 +10,4 @@
<button class="tray__expander" data-action="dialog#toggle" aria-label="Expand pins stack">
</button>
<% end %>
<% end %>
+3
View File
@@ -0,0 +1,3 @@
<%= turbo_frame_tag "pins" do %>
<%= render collection: @pins, partial: "my/pins/pin" %>
<% end %>
@@ -2,10 +2,7 @@
<% content_for :header do %>
<nav class="align-start">
<%= link_to notifications_path, class: "btn flex-item-justify-start", data: { controller: "hotkey", action: "keydown.esc@document->hotkey#click" } do %>
<%= icon_tag "arrow-left" %>
<span class="for-screen-reader">Go back</span>
<% end %>
<%= link_to_back fallback_path: notifications_path %>
<h1 class="txt-x-large"><%= @page_title %></h1>
+3 -6
View File
@@ -2,10 +2,7 @@
<% content_for :header do %>
<nav>
<%= link_to user_path(@user), class: "btn flex-item-justify-start", data: { controller: "hotkey", action: "keydown.esc@document->hotkey#click" } do %>
<%= icon_tag "arrow-left" %>
<span class="for-screen-reader">Back</span>
<% end %>
<%= link_to_back fallback_path: user_path(@user) %>
</nav>
<% end %>
@@ -19,7 +16,7 @@
<label class="btn avatar input--file txt-xx-large center fill-white">
<%= image_tag user_avatar_path(@user), aria: { hidden: "true" }, class: "avatar", size: 128, data: { upload_preview_target: "image" } %>
<%= form.file_field :avatar, id: "file", class: "input", accept: "image/*",
data: { upload_preview_target: "input", action: "upload-preview#previewImage" } %>
data: { upload_preview_target: "input", action: "upload-preview#previewImage" } %>
<span class="for-screen-reader">Profile avatar for <%= @user.name %></span>
</label>
@@ -54,7 +51,7 @@
</div>
<button type="submit" id="log_in" class="btn btn--reversed center">
<%= icon_tag "check" %>
<span class="for-screen-reader">Sign in</span>
<span class="for-screen-reader">Sign in</span>
</button>
<%= link_to "Cancel and go back", user_path(@user), data: { form_target: "cancel", turbo_frame: "_top" }, hidden: true %>
+1 -4
View File
@@ -2,10 +2,7 @@
<% content_for :header do %>
<nav>
<%= link_to "javascript:window.history.back()", class: "btn flex-item-justify-start", data: { controller: "hotkey", action: "keydown.esc@document->hotkey#click" } do %>
<%= icon_tag "arrow-left" %>
<span class="for-screen-reader">Back</span>
<% end %>
<%= link_to_back %>
<% if Current.user == @user %>
<%= link_to edit_user_path(@user), class: "btn flex-item-justify-end", data: { turbo_frame: dom_id(@user) } do %>
+2 -4
View File
@@ -2,10 +2,8 @@
<% content_for :header do %>
<nav>
<%= link_to account_users_path, class: "btn flex-item-justify-start", data: { controller: "hotkey", action: "keydown.esc@document->hotkey#click" } do %>
<%= icon_tag "arrow-left" %>
<span class="for-screen-reader">Go back</span>
<% end %>
<%= link_to_back fallback_path: account_users_path %>
<span class="btn btn--placeholder"></span>
<h1 class="txt-large"><%= @page_title %></h1>
+1 -4
View File
@@ -2,10 +2,7 @@
<% content_for :header do %>
<nav>
<%= link_to workflows_path, class: "btn flex-item-justify-start", data: { controller: "hotkey", action: "keydown.esc@document->hotkey#click" } do %>
<%= icon_tag "arrow-left" %>
<span class="for-screen-reader">Go back</span>
<% end %>
<%= link_to_back fallback_path: workflows_path %>
<h1 class="txt-large center"><%= @page_title %></h1>
<span class="btn btn--placeholder flex-item-justify-end"></span>
</nav>
+6
View File
@@ -0,0 +1,6 @@
#!/usr/bin/env ruby
require_relative "../config/boot"
require "bundler/audit/cli"
ARGV.concat %w[ --config config/bundler-audit.yml ] if ARGV.empty? || ARGV.include?("check")
Bundler::Audit::CLI.start
+2 -8
View File
@@ -1,12 +1,6 @@
#!/usr/bin/env ruby
require_relative "../config/boot"
require "active_support/continuous_integration"
ActiveSupport::ContinuousIntegration.run do
step "Security: Ruby", "bin/brakeman -q"
step "Security: JS", "bin/importmap audit"
step "Style: Ruby", "bin/rubocop"
step "Test: Unit", "bin/rails test"
step "Test: System", "bin/rails test:system"
end
CI = ActiveSupport::ContinuousIntegration
require_relative "../config/ci.rb"
+4 -1
View File
@@ -9,7 +9,7 @@ Bundler.require(*Rails.groups)
module Fizzy
class Application < Rails::Application
# Initialize configuration defaults for originally generated Rails version.
config.load_defaults 8.0
config.load_defaults 8.1
# Include the `lib` directory in autoload paths. Use the `ignore:` option
# to list subdirectories that don't contain `.rb` files or that shouldn't
@@ -23,5 +23,8 @@ module Fizzy
#
# config.time_zone = "Central Time (US & Canada)"
# config.eager_load_paths << Rails.root.join("extras")
# enable load_async
config.active_record.async_query_executor = :global_thread_pool
end
end
+19
View File
@@ -0,0 +1,19 @@
# Run using bin/ci
CI.run do
step "Setup", "bin/setup"
step "Style: Ruby", "bin/rubocop"
step "Security: Gem audit", "bin/bundler-audit check --update"
step "Security: Importmap vulnerability audit", "bin/importmap audit"
step "Security: Brakeman code analysis", "bin/brakeman --quiet --no-pager --exit-on-warn --exit-on-error"
step "Tests: Rails", "bin/rails test"
if success?
step "Signoff: All systems go. Ready for merge and deploy.", "gh signoff"
else
failure "Signoff: CI failed. Do not merge or deploy.", "Fix the issues and try again."
end
end
+28 -35
View File
@@ -8,16 +8,24 @@ Rails.application.routes.draw do
end
end
resolve "Bubble" do |bubble, options|
route_for :bucket_bubble, bubble.bucket, bubble, options
resources :bubbles do
scope module: :bubbles do
resource :engagement
resource :image
resource :pin
resource :pop
resource :publish
resource :reading
resource :recover
resource :watch
resources :assignments
resources :boosts
resources :stagings
resources :taggings
end
end
resolve "Comment" do |comment, options|
options[:anchor] = ActionView::RecordIdentifier.dom_id(comment)
route_for :bucket_bubble, comment.bubble.bucket, comment.bubble, options
end
resources :bubbles
resources :notifications, only: :index
namespace :notifications do
resource :tray, only: :show
@@ -40,30 +48,9 @@ Rails.application.routes.draw do
end
resources :bubbles do
resources :boosts
resources :comments do
resources :reactions, module: :comments
end
resource :readings, only: :create
scope module: :bubbles do
resource :image
resource :pop
resource :engagement
resource :publish
resource :recover
resources :stagings
resource :watch
resource :pin, only: [ :show, :create, :destroy ]
end
namespace :assignments, as: :assignment do
resources :toggles
end
namespace :taggings, as: :tagging do
resources :toggles
end
end
end
@@ -76,6 +63,10 @@ Rails.application.routes.draw do
resource :first_run
resources :qr_codes
namespace :my do
resources :pins
end
resource :session do
scope module: "sessions" do
resources :transfers, only: %i[ show update ]
@@ -100,12 +91,14 @@ Rails.application.routes.draw do
get "up", to: "rails/health#show", as: :rails_health_check
get "manifest" => "rails/pwa#manifest", as: :pwa_manifest
namespace :bubbles do
namespace :pins do
resource :tray, only: :show
end
resources :pins, only: :index
resource :terminal, only: [ :show, :edit ]
resolve "Bubble" do |bubble, options|
route_for :bucket_bubble, bubble.bucket, bubble, options
end
resource :terminal, only: [ :show, :edit ]
resolve "Comment" do |comment, options|
options[:anchor] = ActionView::RecordIdentifier.dom_id(comment)
route_for :bucket_bubble, comment.bubble.bucket, comment.bubble, options
end
end
@@ -0,0 +1,5 @@
class MakeTagTitlesUniquePerAccount < ActiveRecord::Migration[8.1]
def change
add_index :tags, [ :account_id, :title ], unique: true
end
end
Generated
+3 -2
View File
@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[8.1].define(version: 2025_04_04_133310) do
ActiveRecord::Schema[8.1].define(version: 2025_04_07_093421) do
create_table "accesses", force: :cascade do |t|
t.integer "bucket_id", null: false
t.datetime "created_at", null: false
@@ -113,7 +113,7 @@ ActiveRecord::Schema[8.1].define(version: 2025_04_04_133310) do
t.datetime "created_at", null: false
t.integer "creator_id", null: false
t.date "due_on"
t.datetime "last_active_at"
t.datetime "last_active_at", null: false
t.integer "stage_id"
t.text "status", default: "creating", null: false
t.string "title"
@@ -287,6 +287,7 @@ ActiveRecord::Schema[8.1].define(version: 2025_04_04_133310) do
t.datetime "created_at", null: false
t.string "title"
t.datetime "updated_at", null: false
t.index ["account_id", "title"], name: "index_tags_on_account_id_and_title", unique: true
t.index ["account_id"], name: "index_tags_on_account_id"
end

Some files were not shown because too many files have changed in this diff Show More