Merge pull request #386 from basecamp/rename-bubbles-2

Rename bubbles (take 2)
This commit is contained in:
Jorge Manrubia
2025-04-09 16:07:38 +02:00
committed by GitHub
333 changed files with 2998 additions and 2811 deletions

Before

Width:  |  Height:  |  Size: 802 B

After

Width:  |  Height:  |  Size: 802 B

Before

Width:  |  Height:  |  Size: 547 B

After

Width:  |  Height:  |  Size: 547 B

Before

Width:  |  Height:  |  Size: 894 B

After

Width:  |  Height:  |  Size: 894 B

+2 -2
View File
@@ -38,7 +38,7 @@
}
@keyframes wobble {
0% { transform: rotate(calc(var(--bubble-rotate) + 90deg)); }
0% { transform: rotate(calc(var(--card-rotate) + 90deg)); }
15% { border-radius: 66% 34% 72% 28% / 39% 63% 37% 61%; }
25% { border-radius: 55% 47% 62% 40% / 58% 50% 52% 44%; }
33% { border-radius: 46% 54% 61% 39% / 50% 51% 49% 50%; }
@@ -47,7 +47,7 @@
}
@keyframes wobble-alt {
0% { transform: rotate(calc(var(--bubble-rotate) + -90deg)); }
0% { transform: rotate(calc(var(--card-rotate) + -90deg)); }
12% { border-radius: 66% 34% 72% 28% / 39% 63% 37% 61%; }
25% { border-radius: 53% 45% 60% 38% / 56% 48% 50% 42%; }
33% { border-radius: 55% 47% 62% 40% / 58% 50% 52% 44%; }
+24 -24
View File
@@ -20,7 +20,7 @@
}
}
.bubble {
.something-to-rename {
--bubble-background: radial-gradient(circle at center, color(from var(--bubble-color) srgb r g b / 0.25), color(from var(--bubble-color) srgb r g b / 0.05));
--bubble-border-width: 0.2rem;
--hover-size: 0;
@@ -110,15 +110,15 @@
}
@media (hover: hover) {
.bubbles .bubble:not(.popped):hover & {
.bubbles .something-to-rename:not(.popped):hover & {
transform: translate(0, -1rem);
}
.bubbles .bubble:not(.popped):hover & + & {
.bubbles .something-to-rename:not(.popped):hover & + & {
transform: translate(1rem, -1rem);
}
.bubbles .bubble:not(.popped):hover & + & + & {
.bubbles .something-to-rename:not(.popped):hover & + & + & {
transform: translate(1.5rem, -0.5rem);;
}
}
@@ -176,7 +176,7 @@
}
@media (hover: hover) {
.bubbles .bubble:not(.popped):hover & {
.bubbles .something-to-rename:not(.popped):hover & {
transform: translate(-0.5rem, -2rem);
}
}
@@ -195,22 +195,22 @@
}
@media (hover: hover) {
.bubbles .bubble:not(.popped):hover & {
.bubbles .something-to-rename:not(.popped):hover & {
transform: rotate(calc(var(--rotation) + var(--bubble-shift) * 1)) translate(0rem, 0.5rem);
}
}
}
@media (hover: hover) {
.bubbles .bubble:not(.popped):hover & {
.bubbles .something-to-rename:not(.popped):hover & {
transform: translate(1rem, 1rem);
}
.bubbles .bubble:not(.popped):hover & + & {
.bubbles .something-to-rename:not(.popped):hover & + & {
transform: translate(0, -1rem);
}
.bubbles .bubble:not(.popped):hover & + & + & {
.bubbles .something-to-rename:not(.popped):hover & + & + & {
transform: translate(-1rem, -1rem);
}
}
@@ -227,7 +227,7 @@
transition: 0.2s ease;
@media (hover: hover) {
.bubbles .bubble:not(.popped):hover & {
.bubbles .something-to-rename:not(.popped):hover & {
transform: rotate(calc(var(--bubble-rotate) + var(--bubble-shift))) scale(0.9);
}
}
@@ -273,7 +273,7 @@
margin-inline: var(--tray-size);
}
.bubble {
.something-to-rename {
order: 0;
}
@@ -379,22 +379,22 @@
}
}
textarea {
--input-color: var(--bubble-color);
--input-border-radius: 0;
/*textarea {*/
/* --input-color: var(--bubble-color);*/
/* --input-border-radius: 0;*/
font-size: 9cqi;
font-weight: 800;
line-height: 1.2;
/* font-size: 9cqi;*/
/* font-weight: 800;*/
/* line-height: 1.2;*/
&::selection {
background-color: var(--bubble-color);
color: var(--color-ink-reversed);
}
}
/* &::selection {*/
/* background-color: var(--bubble-color);*/
/* color: var(--color-ink-reversed);*/
/* }*/
/*}*/
@media (hover: hover) {
.bubble:has(.bubble__image img:not([src=""])):hover & {
.something-to-rename:has(.bubble__image img:not([src=""])):hover & {
color: var(--color-ink-reversed);
text-shadow: 0 0 0.2em var(--bubble-color), 0 0 0.2em var(--bubble-color), 0 0 0.2em var(--bubble-color);
position: relative;
@@ -422,7 +422,7 @@
--bubble-border-color: color-mix(in srgb, var(--bubble-color) 30%, var(--color-bg));
}
.bubble & {
.something-to-rename & {
--bubble-size: 100%;
}
+25 -25
View File
@@ -78,8 +78,8 @@
--border-size: 0;
aspect-ratio: 2/0.95;
background-color: color-mix(in srgb, var(--bubble-color) 4%, var(--color-bg));
color: color-mix(in srgb, var(--bubble-color) 40%, var(--color-ink));
background-color: color-mix(in srgb, var(--card-color) 4%, var(--color-bg));
color: color-mix(in srgb, var(--card-color) 40%, var(--color-ink));
max-inline-size: 75ch;
z-index: 1;
@@ -113,9 +113,9 @@
.card__actions-container {
--border-radius: 1em;
background-color: var(--color-bg);
color: color-mix(in srgb, var(--bubble-color) 40%, var(--color-ink));
color: color-mix(in srgb, var(--card-color) 40%, var(--color-ink));
margin-block: calc(var(--block-space) * -1.66) calc(var(--block-space) * 1.66);
padding: calc(var(--block-space) * 0.66) calc(var(--inline-space) * 1.5);
position: relative;
@@ -126,18 +126,18 @@
}
.btn:not(.popup__item, .btn--plain) {
--btn-background: var(--bubble-color);
--btn-background: var(--card-color);
--btn-color: var(--color-ink-reversed);
}
.btn--plain {
--btn-color: var(--bubble-color);
--btn-color: var(--card-color);
text-decoration: underline;
}
.bubble__pop-message {
color: var(--bubble-color);
.card__closure-message {
color: var(--card-color);
}
}
@@ -152,7 +152,7 @@
}
.switch__input:checked + .switch__btn {
background-color: var(--bubble-color) !important;
background-color: var(--card-color) !important;
}
strong {
@@ -203,14 +203,14 @@
padding-block: var(--block-space);
}
.card__bucket {
background-color: var(--bubble-color);
.card__collection {
background-color: var(--card-color);
font-weight: 800;
letter-spacing: 0.5ch;
padding: var(--block-space-half) var(--inline-space-double);
}
.card-bucket__filter {
.card-collection__filter {
> button {
--hover-size: 0;
}
@@ -244,7 +244,7 @@
--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: color-mix(in srgb, var(--bubble-color) 33%, var(--color-bg));
background-color: color-mix(in srgb, var(--card-color) 33%, var(--color-bg));
border-radius: 0.2em;
display: inline-flex;
margin-block: calc(var(--block-space) * -1) calc(var(--block-space) * -0.33);
@@ -389,7 +389,7 @@
}
.card__meta {
--border-color: var(--bubble-color);
--border-color: var(--card-color);
--border-size: 1px;
--row-gap: 0.25em;
@@ -405,7 +405,7 @@
}
.card__move-button {
--btn-background: color-mix(in srgb, var(--bubble-color) 40%, var(--color-ink));
--btn-background: color-mix(in srgb, var(--card-color) 40%, var(--color-ink));
margin-block-start: calc(var(--block-space) * 1.2);
margin-inline-end: calc(var(--inline-space) * -4);
@@ -417,12 +417,12 @@
}
.card__tag {
color: var(--bubble-color);
color: var(--card-color);
font-weight: 700;
}
.card__tags {
color: var(--bubble-color);
color: var(--card-color);
}
.card__title {
@@ -498,10 +498,10 @@
}
}
.card__popped {
.card__closed {
align-items: center;
aspect-ratio: 7/4;
border: var(--block-space-half) solid var(--bubble-color);
border: var(--block-space-half) solid var(--card-color);
position: absolute;
inset: auto var(--block-space) var(--block-space-double) auto;
justify-content: space-between;
@@ -510,18 +510,18 @@
z-index: -1;
}
.card__popped-by {
.card__closed-by {
border-block-end: 1px dashed currentcolor;
}
.card__popped-title {
.card__closed-title {
color: var(--color-ink-reversed);
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(--card-color),
2px -2px 0 var(--card-color),
-2px 2px 0 var(--card-color),
2px 2px 0 var(--card-color);
}
}
@@ -1,5 +1,5 @@
@layer components {
.bucket {
.collection {
inline-size: min(35ch, 40%);
line-height: 1.2;
@@ -10,7 +10,7 @@
}
}
.buckets {
.collections {
--gap: 2cqi;
--hover-size: 0;
--column-gap: var(--gap);
@@ -20,12 +20,12 @@
padding: var(--gap);
}
.bucket__button {
.collection__button {
position: absolute;
inset: calc((var(--btn-size) * -1) - 2cqi) 1cqi auto auto;
}
.bucket__bubbles {
.collection__cards {
--border-radius: 1.5em;
margin-block: 0;
@@ -33,7 +33,7 @@
padding: 1cqi;
}
.bubbles__container {
.cards__container {
aspect-ratio: 18 / 9;
container-type: inline-size;
}
+1 -1
View File
@@ -1,5 +1,5 @@
.color-picker {
--panel-border-color: var(--bubble-color);
--panel-border-color: var(--card-color);
--panel-border-radius: 2em;
--panel-border-size: 2px;
--panel-padding: 0.5em 0.7em;
+2 -2
View File
@@ -86,7 +86,7 @@
}
.event--related {
--hover-color: var(--bubble-color);
--hover-color: var(--card-color);
--hover-size: 0.15rem;
box-shadow: 0 0 0 var(--hover-size) var(--hover-color);
@@ -138,7 +138,7 @@
.event__icon {
aspect-ratio: 1;
background-color: var(--bubble-color);
background-color: var(--card-color);
block-size: 1.7em;
border-radius: 1.7em;
inline-size: 1.7em;
+3 -3
View File
@@ -30,8 +30,8 @@
.icon--bolt { --svg: url("bolt.svg "); }
.icon--bookmark-outline { --svg: url("bookmark-outline.svg "); }
.icon--bookmark { --svg: url("bookmark.svg "); }
.icon--bubble-add { --svg: url("bubble-add.svg "); }
.icon--bubbles { --svg: url("bubbles.svg "); }
.icon--card-add { --svg: url("card-add.svg "); }
.icon--cards { --svg: url("cards.svg "); }
.icon--calendar-add { --svg: url("calendar-add.svg "); }
.icon--calendar { --svg: url("calendar.svg "); }
.icon--camera { --svg: url("camera.svg "); }
@@ -65,7 +65,7 @@
.icon--picture-double { --svg: url("picture-double.svg "); }
.icon--picture-remove { --svg: url("picture-remove.svg "); }
.icon--pinned { --svg: url("pinned.svg "); }
.icon--pop { --svg: url("pop.svg "); }
.icon--closure { --svg: url("closure.svg "); }
.icon--qr-code { --svg: url("qr-code.svg "); }
.icon--reaction { --svg: url("reaction.svg "); }
.icon--refresh { --svg: url("refresh.svg "); }
+3 -3
View File
@@ -14,8 +14,8 @@
}
}
.pin__bucket {
background-color: var(--bubble-color);
.pin__collection {
background-color: var(--card-color);
font-weight: 800;
letter-spacing: 0.3ch;
padding: 0.3em 0.6em 0.3em 1.5em;
@@ -45,7 +45,7 @@
}
.pin__tags {
color: var(--bubble-color);
color: var(--card-color);
}
.pin__title {
+6 -6
View File
@@ -11,35 +11,35 @@
@media (hover: hover) {
&:hover {
background-color: color(from var(--bubble-color) srgb r g b / 0.15);
background-color: color(from var(--card-color) srgb r g b / 0.15);
}
}
.bubbles-list & {
.cards-list & {
display: none;
}
}
.workflow-stage--current {
--btn-background: var(--bubble-color);
--btn-background: var(--card-color);
color: var(--color-ink-reversed);
@media (hover: hover) {
&:hover {
background-color: color(from var(--bubble-color) srgb r g b / 0.65);
background-color: color(from var(--card-color) srgb r g b / 0.65);
}
}
.ticket & {
--btn-background: var(--color-ink);
background: var(--bubble-color);
background: var(--card-color);
border-radius: 0.2em;
color: var(--color-ink-reversed);
}
.bubbles-list & {
.cards-list & {
display: flex;
background: transparent;
border-radius: 0.2em;
@@ -1,10 +0,0 @@
class Bubbles::AssignmentsController < ApplicationController
include BubbleScoped
def new
end
def create
@bubble.toggle_assignment @bucket.users.active.find(params[:assignee_id])
end
end
@@ -1,12 +0,0 @@
class Bubbles::BoostsController < ApplicationController
include BubbleScoped
def create
count = if params[:boost_count].to_i == @bubble.boosts_count
@bubble.boosts_count + 1
else
params[:boost_count].to_i
end
@bubble.boost!(count)
end
end
@@ -1,13 +0,0 @@
class Bubbles::EngagementsController < ApplicationController
include BubbleScoped
def create
@bubble.engage
redirect_to @bubble
end
def destroy
@bubble.reconsider
redirect_to @bubble
end
end
@@ -1,8 +0,0 @@
class Bubbles::ImagesController < ApplicationController
include BubbleScoped
def destroy
@bubble.image.purge_later
redirect_to @bubble
end
end
@@ -1,13 +0,0 @@
class Bubbles::PopsController < ApplicationController
include BubbleScoped
def create
@bubble.pop!(user: Current.user, reason: params[:reason])
redirect_to @bubble
end
def destroy
@bubble.unpop
redirect_to @bubble
end
end
@@ -1,8 +0,0 @@
class Bubbles::PublishesController < ApplicationController
include BubbleScoped
def create
@bubble.publish
redirect_to @bubble
end
end
@@ -1,13 +0,0 @@
class Bubbles::ReadingsController < ApplicationController
include BubbleScoped
def create
mark_bubble_notifications_read
@notifications = Current.user.notifications.unread.ordered.limit(20)
end
private
def mark_bubble_notifications_read
Current.user.notifications.unread.where(bubble: @bubble).read_all
end
end
@@ -1,7 +0,0 @@
class Bubbles::RecoversController < ApplicationController
include BubbleScoped
def create
redirect_to @bubble.recover_abandoned_creation
end
end
@@ -1,11 +0,0 @@
class Bubbles::StagingsController < ApplicationController
include BubbleScoped
def create
if params[:stage_id].present?
@bubble.toggle_stage Current.account.stages.find(params[:stage_id])
else
@bubble.update!(stage: nil)
end
end
end
@@ -1,13 +0,0 @@
class Bubbles::WatchesController < ApplicationController
include BubbleScoped
def create
@bubble.watch_by Current.user
redirect_to bubble_watch_path(@bubble)
end
def destroy
@bubble.unwatch_by Current.user
redirect_to bubble_watch_path(@bubble)
end
end
-71
View File
@@ -1,71 +0,0 @@
class BubblesController < ApplicationController
include BucketScoped
skip_before_action :set_bucket, only: :index
before_action :set_filter, only: :index
before_action :set_bubble, only: %i[ show edit update destroy ]
before_action :handle_display_count, only: :index
DISPLAY_COUNT_OPTIONS = [ 6, 12, 18, 24 ].freeze
DEFAULT_DISPLAY_COUNT = 6
RECENTLY_POPPED_LIMIT = 100
def index
@considering_bubbles = @filter.bubbles.considering.load_async
@doing_bubbles = @filter.bubbles.doing.load_async
@popped_bubbles = @filter.with(indexed_by: "popped").bubbles.recently_popped_first.limit(RECENTLY_POPPED_LIMIT).load_async
end
def create
redirect_to @bucket.bubbles.create!
end
def show
end
def edit
end
def destroy
@bubble.destroy!
redirect_to bubbles_path(bucket_ids: [ @bubble.bucket ]), notice: deleted_notice
end
def update
@bubble.update! bubble_params
redirect_to @bubble
end
private
DEFAULT_PARAMS = { indexed_by: "newest" }
def set_filter
@filter = Current.user.filters.from_params params.reverse_merge(**DEFAULT_PARAMS).permit(*Filter::PERMITTED_PARAMS)
end
def set_bubble
@bubble = @bucket.bubbles.find params[:id]
end
def bubble_params
params.expect(bubble: [ :status, :title, :color, :due_on, :image, :draft_comment, tag_ids: [] ])
end
def deleted_notice
"Bubble deleted" unless @bubble.creating?
end
def handle_display_count
if params[:set_display_count].present?
cookies[:display_count] = params[:set_display_count]
redirect_to bubbles_path(
params.permit(*Filter::PERMITTED_PARAMS, :bucket_ids).except(:set_display_count)
)
end
end
def display_count
(cookies[:display_count] || DEFAULT_DISPLAY_COUNT).to_i
end
end
@@ -1,7 +0,0 @@
class Buckets::InvolvementsController < ApplicationController
include BucketScoped
def update
@bucket.access_for(Current.user).update!(involvement: params[:involvement])
end
end
@@ -1,16 +0,0 @@
class Buckets::WorkflowsController < ApplicationController
include BucketScoped
before_action :set_workflow
def update
@bucket.update! workflow: @workflow
redirect_to bubbles_path(bucket_ids: [ @bucket ])
end
private
def set_workflow
@workflow = Current.account.workflows.find(params.expect(bucket: [ :workflow_id ]).require(:workflow_id))
end
end
-50
View File
@@ -1,50 +0,0 @@
class BucketsController < ApplicationController
before_action :set_bucket, except: %i[ new create ]
def new
@bucket = Current.account.buckets.build
end
def create
@bucket = Current.account.buckets.create! bucket_params
redirect_to bubbles_path(bucket_ids: [ @bucket ])
end
def edit
selected_user_ids = @bucket.users.pluck :id
@selected_users, @unselected_users = User.active.alphabetically.partition { |user| selected_user_ids.include? user.id }
end
def update
@bucket.update! bucket_params
@bucket.accesses.revise granted: grantees, revoked: revokees
redirect_to bubbles_path(bucket_ids: [ @bucket ])
end
def destroy
@bucket.destroy
redirect_to root_path
end
private
def set_bucket
@bucket = Current.user.buckets.find params[:id]
end
def bucket_params
params.expect(bucket: [ :name, :all_access ]).with_defaults(all_access: true)
end
def grantees
Current.account.users.active.where id: grantee_ids
end
def revokees
@bucket.users.where.not id: grantee_ids
end
def grantee_ids
params.fetch :user_ids, []
end
end
@@ -0,0 +1,10 @@
class Cards::AssignmentsController < ApplicationController
include CardScoped
def new
end
def create
@card.toggle_assignment @collection.users.active.find(params[:assignee_id])
end
end
@@ -0,0 +1,12 @@
class Cards::BoostsController < ApplicationController
include CardScoped
def create
count = if params[:boost_count].to_i == @card.boosts_count
@card.boosts_count + 1
else
params[:boost_count].to_i
end
@card.boost!(count)
end
end
@@ -0,0 +1,13 @@
class Cards::ClosuresController < ApplicationController
include CardScoped
def create
@card.close(user: Current.user, reason: params[:reason])
redirect_to @card
end
def destroy
@card.reopen
redirect_to @card
end
end
@@ -0,0 +1,13 @@
class Cards::EngagementsController < ApplicationController
include CardScoped
def create
@card.engage
redirect_to @card
end
def destroy
@card.reconsider
redirect_to @card
end
end
@@ -0,0 +1,8 @@
class Cards::ImagesController < ApplicationController
include CardScoped
def destroy
@card.image.purge_later
redirect_to @card
end
end
@@ -1,21 +1,21 @@
class Bubbles::PinsController < ApplicationController
include BubbleScoped
class Cards::PinsController < ApplicationController
include CardScoped
def show
end
def create
pin = @bubble.pin_by Current.user
pin = @card.pin_by Current.user
broadcast_my_new pin
redirect_to bubble_pin_path(@bubble)
redirect_to card_pin_path(@card)
end
def destroy
pin = @bubble.unpin_by Current.user
pin = @card.unpin_by Current.user
broadcast_my_removed pin
redirect_to bubble_pin_path(@bubble)
redirect_to card_pin_path(@card)
end
private
@@ -0,0 +1,8 @@
class Cards::PublishesController < ApplicationController
include CardScoped
def create
@card.publish
redirect_to @card
end
end
@@ -0,0 +1,13 @@
class Cards::ReadingsController < ApplicationController
include CardScoped
def create
mark_card_notifications_read
@notifications = Current.user.notifications.unread.ordered.limit(20)
end
private
def mark_card_notifications_read
Current.user.notifications.unread.where(card: @card).read_all
end
end
@@ -0,0 +1,7 @@
class Cards::RecoversController < ApplicationController
include CardScoped
def create
redirect_to @card.recover_abandoned_creation
end
end
@@ -0,0 +1,11 @@
class Cards::StagingsController < ApplicationController
include CardScoped
def create
if params[:stage_id].present?
@card.toggle_stage Current.account.stages.find(params[:stage_id])
else
@card.update!(stage: nil)
end
end
end
@@ -1,12 +1,12 @@
class Bubbles::TaggingsController < ApplicationController
include BubbleScoped
class Cards::TaggingsController < ApplicationController
include CardScoped
def new
@tags = Current.account.tags.alphabetically
end
def create
@bubble.toggle_tag_with sanitized_tag_title_param
@card.toggle_tag_with sanitized_tag_title_param
end
private
@@ -0,0 +1,13 @@
class Cards::WatchesController < ApplicationController
include CardScoped
def create
@card.watch_by Current.user
redirect_to card_watch_path(@card)
end
def destroy
@card.unwatch_by Current.user
redirect_to card_watch_path(@card)
end
end
+71
View File
@@ -0,0 +1,71 @@
class CardsController < ApplicationController
include CollectionScoped
skip_before_action :set_collection, only: :index
before_action :set_filter, only: :index
before_action :set_card, only: %i[ show edit update destroy ]
before_action :handle_display_count, only: :index
DISPLAY_COUNT_OPTIONS = [ 6, 12, 18, 24 ].freeze
DEFAULT_DISPLAY_COUNT = 6
RECENTLY_CLOSED_LIMIT = 100
def index
@considering_cards = @filter.cards.considering.load_async
@doing_cards = @filter.cards.doing.load_async
@closed_cards = @filter.with(indexed_by: "closed").cards.recently_closed_first.limit(RECENTLY_CLOSED_LIMIT).load_async
end
def create
redirect_to @collection.cards.create!
end
def show
end
def edit
end
def destroy
@card.destroy!
redirect_to cards_path(collection_ids: [ @card.collection ]), notice: deleted_notice
end
def update
@card.update! card_params
redirect_to @card
end
private
DEFAULT_PARAMS = { indexed_by: "newest" }
def set_filter
@filter = Current.user.filters.from_params params.reverse_merge(**DEFAULT_PARAMS).permit(*Filter::PERMITTED_PARAMS)
end
def set_card
@card = @collection.cards.find params[:id]
end
def card_params
params.expect(card: [ :status, :title, :color, :due_on, :image, :draft_comment, tag_ids: [] ])
end
def deleted_notice
"Card deleted" unless @card.creating?
end
def handle_display_count
if params[:set_display_count].present?
cookies[:display_count] = params[:set_display_count]
redirect_to cards_path(
params.permit(*Filter::PERMITTED_PARAMS, :collection_ids).except(:set_display_count)
)
end
end
def display_count
(cookies[:display_count] || DEFAULT_DISPLAY_COUNT).to_i
end
end
@@ -0,0 +1,7 @@
class Collections::InvolvementsController < ApplicationController
include CollectionScoped
def update
@collection.access_for(Current.user).update!(involvement: params[:involvement])
end
end
@@ -0,0 +1,16 @@
class Collections::WorkflowsController < ApplicationController
include CollectionScoped
before_action :set_workflow
def update
@collection.update! workflow: @workflow
redirect_to cards_path(collection_ids: [ @collection ])
end
private
def set_workflow
@workflow = Current.account.workflows.find(params.expect(collection: [ :workflow_id ]).require(:workflow_id))
end
end
+50
View File
@@ -0,0 +1,50 @@
class CollectionsController < ApplicationController
before_action :set_collection, except: %i[ new create ]
def new
@collection = Current.account.collections.build
end
def create
@collection = Current.account.collections.create! collection_params
redirect_to cards_path(collection_ids: [ @collection ])
end
def edit
selected_user_ids = @collection.users.pluck :id
@selected_users, @unselected_users = User.active.alphabetically.partition { |user| selected_user_ids.include? user.id }
end
def update
@collection.update! collection_params
@collection.accesses.revise granted: grantees, revoked: revokees
redirect_to cards_path(collection_ids: [ @collection ])
end
def destroy
@collection.destroy
redirect_to root_path
end
private
def set_collection
@collection = Current.user.collections.find params[:id]
end
def collection_params
params.expect(collection: [ :name, :all_access ]).with_defaults(all_access: true)
end
def grantees
Current.account.users.active.where id: grantee_ids
end
def revokees
@collection.users.where.not id: grantee_ids
end
def grantee_ids
params.fetch :user_ids, []
end
end
@@ -12,7 +12,7 @@ class Comments::ReactionsController < ApplicationController
@reaction = @comment.reactions.create!(reaction_params)
broadcast_create
redirect_to bucket_bubble_comment_reactions_url(@comment.bubble.bucket, @comment.bubble, @comment)
redirect_to collection_card_comment_reactions_url(@comment.card.collection, @comment.card, @comment)
end
def destroy
+4 -4
View File
@@ -1,10 +1,10 @@
class CommentsController < ApplicationController
include BubbleScoped
include CardScoped
before_action :set_comment, only: [ :show, :edit, :update, :destroy ]
before_action :require_own_comment, only: [ :edit, :update, :destroy ]
def create
@bubble.capture new_comment
@card.capture new_comment
end
def show
@@ -19,7 +19,7 @@ class CommentsController < ApplicationController
def destroy
@comment.destroy
redirect_to @bubble
redirect_to @card
end
private
@@ -33,7 +33,7 @@ class CommentsController < ApplicationController
def set_comment
@comment = Comment.joins(:message)
.where(messages: { bubble_id: @bubble.id })
.where(messages: { card_id: @card.id })
.find(params[:id])
end
-16
View File
@@ -1,16 +0,0 @@
module BubbleScoped
extend ActiveSupport::Concern
included do
before_action :set_bubble, :set_bucket
end
private
def set_bubble
@bubble = Current.user.accessible_bubbles.find(params[:bubble_id])
end
def set_bucket
@bucket = Current.user.buckets.find(@bubble.bucket_id)
end
end
@@ -1,24 +0,0 @@
module BucketFilterable
extend ActiveSupport::Concern
included do
before_action :set_bucket_filter
end
private
def set_bucket_filter
params[:bucket_ids] ||= cookies[:bucket_filter]&.split(",") unless params[:clear_filter]
end
def bucket_filter
params[:bucket_ids].presence || Current.user.bucket_ids
end
def update_bucket_filter
if params[:clear_filter]
cookies.delete(:bucket_filter)
elsif params[:bucket_ids].present?
cookies[:bucket_filter] = params[:bucket_ids].join(",")
end
end
end
-12
View File
@@ -1,12 +0,0 @@
module BucketScoped
extend ActiveSupport::Concern
included do
before_action :set_bucket
end
private
def set_bucket
@bucket = Current.user.buckets.find(params[:bucket_id])
end
end
+16
View File
@@ -0,0 +1,16 @@
module CardScoped
extend ActiveSupport::Concern
included do
before_action :set_card, :set_collection
end
private
def set_card
@card = Current.user.accessible_cards.find(params[:card_id])
end
def set_collection
@collection = Current.user.collections.find(@card.collection_id)
end
end
@@ -0,0 +1,24 @@
module CollectionFilterable
extend ActiveSupport::Concern
included do
before_action :set_collection_filter
end
private
def set_collection_filter
params[:collection_ids] ||= cookies[:collection_filter]&.split(",") unless params[:clear_filter]
end
def collection_filter
params[:collection_ids].presence || Current.user.collection_ids
end
def update_collection_filter
if params[:clear_filter]
cookies.delete(:collection_filter)
elsif params[:collection_ids].present?
cookies[:collection_filter] = params[:collection_ids].join(",")
end
end
end
@@ -0,0 +1,12 @@
module CollectionScoped
extend ActiveSupport::Concern
included do
before_action :set_collection
end
private
def set_collection
@collection = Current.user.collections.find(params[:collection_id])
end
end
+6 -6
View File
@@ -1,7 +1,7 @@
module EventsTimeline
extend ActiveSupport::Concern
include BucketFilterable
include CollectionFilterable
included do
before_action :set_activity_day
@@ -23,7 +23,7 @@ module EventsTimeline
group_by { |event| [ event.created_at.hour, helpers.event_column(event) ] }.
map { |hour_col, events|
[ hour_col,
events.uniq { |e| e.action == "boosted" ? [ e.creator_id, e.bubble_id ] : e.id }
events.uniq { |e| e.action == "boosted" ? [ e.creator_id, e.card_id ] : e.id }
]
}
end
@@ -33,12 +33,12 @@ module EventsTimeline
end
def user_events
Event.where(bubble: user_bubbles, creator: Current.account.users)
Event.where(card: user_cards, creator: Current.account.users)
end
def user_bubbles
Current.user.accessible_bubbles
def user_cards
Current.user.accessible_cards
.published_or_drafted_by(Current.user)
.where(bucket_id: bucket_filter)
.where(collection_id: collection_filter)
end
end
+2 -2
View File
@@ -2,9 +2,9 @@ class EventsController < ApplicationController
include EventsTimeline
def index
update_bucket_filter
update_collection_filter
@buckets = Current.user.buckets.alphabetically
@collections = Current.user.collections.alphabetically
@filters = Current.user.filters.all
@events = events_for_activity_day
+2 -2
View File
@@ -3,7 +3,7 @@ class FiltersController < ApplicationController
def create
@filter = Current.user.filters.remember filter_params
redirect_to bubbles_path(@filter.as_params)
redirect_to cards_path(@filter.as_params)
end
def destroy
@@ -24,7 +24,7 @@ class FiltersController < ApplicationController
if request.referer == root_url
redirect_to root_path
else
redirect_to bubbles_path(@filter.as_params)
redirect_to cards_path(@filter.as_params)
end
end
end
+1 -1
View File
@@ -1,5 +1,5 @@
class My::PinsController < ApplicationController
def index
@pins = Current.user.pins.includes(:bubble).ordered.limit(20)
@pins = Current.user.pins.includes(:card).ordered.limit(20)
end
end
@@ -1,7 +1,7 @@
module Notifications
class SettingsController < ApplicationController
def show
@buckets = Current.user.buckets.alphabetically
@collections = Current.user.collections.alphabetically
end
end
end
+3 -3
View File
@@ -1,6 +1,6 @@
class TerminalsController < ApplicationController
def show
@events = Event.where(bubble: user_bubbles, creator: Current.user).chronologically.reverse_order.limit(20)
@events = Event.where(card: user_cards, creator: Current.user).chronologically.reverse_order.limit(20)
end
def edit
@@ -8,7 +8,7 @@ class TerminalsController < ApplicationController
end
private
def user_bubbles
Current.user.accessible_bubbles
def user_cards
Current.user.accessible_cards
end
end
+14 -14
View File
@@ -1,29 +1,29 @@
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: {
def access_menu_tag(collection, &)
tag.menu class: [ "flex flex-column gap margin-none pad txt-medium", { "toggler--toggled": collection.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",
render partial: "collections/access_toggle",
collection: users, as: :user,
locals: { selected: selected },
cached: ->(user) { [ user, selected ] }
end
def access_involvement_advance_button(bucket, user)
access = bucket.access_for(user)
def access_involvement_advance_button(collection, user)
access = collection.access_for(user)
turbo_frame_tag dom_id(bucket, :involvement_button) do
button_to bucket_involvement_path(bucket), method: :put,
aria: { labelledby: dom_id(bucket, :involvement_label) },
turbo_frame_tag dom_id(collection, :involvement_button) do
button_to collection_involvement_path(collection), method: :put,
aria: { labelledby: dom_id(collection, :involvement_label) },
class: [ "btn", { "btn--reversed": access.involvement == "watching" || access.involvement == "everything" } ],
params: { involvement: next_involvement(access.involvement) },
title: involvement_access_label(bucket, access.involvement) do
title: involvement_access_label(collection, access.involvement) do
icon_tag("notification-bell-#{access.involvement.dasherize}") +
tag.span(involvement_access_label(bucket, access.involvement), class: "for-screen-reader", id: dom_id(bucket, :involvement_label))
tag.span(involvement_access_label(collection, access.involvement), class: "for-screen-reader", id: dom_id(collection, :involvement_label))
end
end
end
@@ -34,14 +34,14 @@ module AccessesHelper
order[(order.index(involvement.to_s) + 1) % order.size]
end
def involvement_access_label(bucket, involvement)
def involvement_access_label(collection, involvement)
case involvement
when "access_only"
"Notifications are off for #{bucket.name}"
"Notifications are off for #{collection.name}"
when "everything"
"Notifying me about everything in #{bucket.name}"
"Notifying me about everything in #{collection.name}"
when "watching"
"Notifying me only about @mentions and new items in #{bucket.name}"
"Notifying me only about @mentions and new items in #{collection.name}"
end
end
end
-24
View File
@@ -1,24 +0,0 @@
module BubblesHelper
BUBBLE_ROTATION = %w[ 75 60 45 35 25 5 ]
def bubble_title(bubble)
bubble.title.presence || "Untitled"
end
def bubble_rotation(bubble)
value = BUBBLE_ROTATION[Zlib.crc32(bubble.to_param) % BUBBLE_ROTATION.size]
"--bubble-rotate: #{value}deg;"
end
def display_count_options
BubblesController::DISPLAY_COUNT_OPTIONS.map do |count|
{
value: count,
label: count,
selected: @display_count == count,
id: "display-count-#{count}"
}
end
end
end
+24
View File
@@ -0,0 +1,24 @@
module CardsHelper
CARD_ROTATION = %w[ 75 60 45 35 25 5 ]
def card_title(card)
card.title.presence || "Untitled"
end
def card_rotation(card)
value = CARD_ROTATION[Zlib.crc32(card.to_param) % CARD_ROTATION.size]
"--card-rotate: #{value}deg;"
end
def display_count_options
CardsController::DISPLAY_COUNT_OPTIONS.map do |count|
{
value: count,
label: count,
selected: @display_count == count,
id: "display-count-#{count}"
}
end
end
end
+2 -2
View File
@@ -4,8 +4,8 @@ module CommentsHelper
data: { creator_id: comment.creator_id, created_by_current_user_target: "creation" }, &
end
def new_comment_placeholder(bubble)
if bubble.creator == Current.user && bubble.messages.comments.empty?
def new_comment_placeholder(card)
if card.creator == Current.user && card.messages.comments.empty?
"Next, add some notes, context, pictures, or video about this…"
else
"Type your comment…"
+19 -19
View File
@@ -12,7 +12,7 @@ module EventsHelper
def event_column(event)
case event.action
when "popped"
when "closed"
3
when "published"
1
@@ -45,15 +45,15 @@ module EventsHelper
start_time = day.beginning_of_day
end_time = day.end_of_day
accessible_events = Event.joins(bubble: :bucket)
.merge(Current.user.buckets)
accessible_events = Event.joins(card: :collection)
.merge(Current.user.collections)
.where(created_at: start_time..end_time)
.where(bubbles: { bucket_id: params[:bucket_ids].presence || Current.user.bucket_ids })
.where(cards: { collection_id: params[:collection_ids].presence || Current.user.collection_ids })
headers = {
"Added" => accessible_events.where(action: "published").count,
"Updated" => nil,
"Closed" => accessible_events.where(action: "popped").count
"Closed" => accessible_events.where(action: "closed").count
}
headers.map do |header, count|
@@ -66,32 +66,32 @@ module EventsHelper
case event.action
when "assigned"
if event.assignees.include?(Current.user)
"#{ event.creator == Current.user ? "You" : event.creator.name } will handle <span style='color: var(--bubble-color)'>#{ bubble_title(event.bubble) }</span>".html_safe
"#{ event.creator == Current.user ? "You" : event.creator.name } will handle <span style='color: var(--card-color)'>#{ card_title(event.card) }</span>".html_safe
else
"#{ event.creator == Current.user ? "You" : event.creator.name } assigned #{ event.assignees.pluck(:name).to_sentence } to <span style='color: var(--bubble-color)'>#{ bubble_title(event.bubble) }</span>".html_safe
"#{ event.creator == Current.user ? "You" : event.creator.name } assigned #{ event.assignees.pluck(:name).to_sentence } to <span style='color: var(--card-color)'>#{ card_title(event.card) }</span>".html_safe
end
when "unassigned"
"#{ event.creator == Current.user ? "You" : event.creator.name } unassigned #{ event.assignees.pluck(:name).to_sentence } from <span style='color: var(--bubble-color)'>#{ bubble_title(event.bubble) }</span>".html_safe
"#{ event.creator == Current.user ? "You" : event.creator.name } unassigned #{ event.assignees.pluck(:name).to_sentence } from <span style='color: var(--card-color)'>#{ card_title(event.card) }</span>".html_safe
when "boosted"
"#{ event.creator == Current.user ? "You" : event.creator.name } boosted <span style='color: var(--bubble-color)'>#{ bubble_title(event.bubble) }</span>".html_safe
"#{ event.creator == Current.user ? "You" : event.creator.name } boosted <span style='color: var(--card-color)'>#{ card_title(event.card) }</span>".html_safe
when "commented"
"#{ event.creator == Current.user ? "You" : event.creator.name } commented on <span style='color: var(--bubble-color)'>#{ bubble_title(event.bubble) }</span>".html_safe
"#{ event.creator == Current.user ? "You" : event.creator.name } commented on <span style='color: var(--card-color)'>#{ card_title(event.card) }</span>".html_safe
when "published"
"#{ event.creator == Current.user ? "You" : event.creator.name } added <span style='color: var(--bubble-color)'>#{ bubble_title(event.bubble) }</span>".html_safe
when "popped"
"#{ event.creator == Current.user ? "You" : event.creator.name } closed <span style='color: var(--bubble-color)'>#{ bubble_title(event.bubble) }</span>".html_safe
"#{ event.creator == Current.user ? "You" : event.creator.name } added <span style='color: var(--card-color)'>#{ card_title(event.card) }</span>".html_safe
when "closed"
"#{ event.creator == Current.user ? "You" : event.creator.name } closed <span style='color: var(--card-color)'>#{ card_title(event.card) }</span>".html_safe
when "staged"
"#{event.creator == Current.user ? "You" : event.creator.name} changed the stage to #{event.stage_name} on <span style='color: var(--bubble-color)'>#{ bubble_title(event.bubble) }</span>".html_safe
"#{event.creator == Current.user ? "You" : event.creator.name} changed the stage to #{event.stage_name} on <span style='color: var(--card-color)'>#{ card_title(event.card) }</span>".html_safe
when "unstaged"
"#{event.creator == Current.user ? "You" : event.creator.name} removed <span style='color: var(--bubble-color)'>#{ bubble_title(event.bubble) }</span> from the #{event.stage_name} stage".html_safe
"#{event.creator == Current.user ? "You" : event.creator.name} removed <span style='color: var(--card-color)'>#{ card_title(event.card) }</span> from the #{event.stage_name} stage".html_safe
when "due_date_added"
"#{event.creator == Current.user ? "You" : event.creator.name} set the date to #{event.particulars.dig('particulars', 'due_date').to_date.strftime('%B %-d')} on <span style='color: var(--bubble-color)'>#{ bubble_title(event.bubble) }</span>".html_safe
"#{event.creator == Current.user ? "You" : event.creator.name} set the date to #{event.particulars.dig('particulars', 'due_date').to_date.strftime('%B %-d')} on <span style='color: var(--card-color)'>#{ card_title(event.card) }</span>".html_safe
when "due_date_changed"
"#{event.creator == Current.user ? "You" : event.creator.name} changed the date to #{event.particulars.dig('particulars', 'due_date').to_date.strftime('%B %-d')} on <span style='color: var(--bubble-color)'>#{ bubble_title(event.bubble) }</span>".html_safe
"#{event.creator == Current.user ? "You" : event.creator.name} changed the date to #{event.particulars.dig('particulars', 'due_date').to_date.strftime('%B %-d')} on <span style='color: var(--card-color)'>#{ card_title(event.card) }</span>".html_safe
when "due_date_removed"
"#{event.creator == Current.user ? "You" : event.creator.name} removed the date on <span style='color: var(--bubble-color)'>#{ bubble_title(event.bubble) }</span>"
"#{event.creator == Current.user ? "You" : event.creator.name} removed the date on <span style='color: var(--card-color)'>#{ card_title(event.card) }</span>"
when "title_changed"
"#{event.creator == Current.user ? "You" : event.creator.name} renamed on <span style='color: var(--bubble-color)'>#{ bubble_title(event.bubble) }</span> (was: '#{event.particulars.dig('particulars', 'old_title')})'".html_safe
"#{event.creator == Current.user ? "You" : event.creator.name} renamed on <span style='color: var(--card-color)'>#{ card_title(event.card) }</span> (was: '#{event.particulars.dig('particulars', 'old_title')})'".html_safe
end
end
+5 -5
View File
@@ -1,16 +1,16 @@
module FiltersHelper
def filter_title(filter)
if filter.buckets.none?
if filter.collections.none?
"All collections"
elsif filter.buckets.one?
filter.buckets.first.name
elsif filter.collections.one?
filter.collections.first.name
else
filter.buckets.map(&:name).to_sentence
filter.collections.map(&:name).to_sentence
end
end
def filter_chip_tag(text, params)
link_to bubbles_path(params), class: "btn txt-small btn--remove fill-selected" do
link_to cards_path(params), class: "btn txt-small btn--remove fill-selected" do
concat tag.span(text)
concat icon_tag("close")
end
+3 -3
View File
@@ -1,8 +1,8 @@
module MessagesHelper
def messages_tag(bubble, &)
turbo_frame_tag dom_id(bubble, :messages),
def messages_tag(card, &)
turbo_frame_tag dom_id(card, :messages),
class: "comments gap center",
style: "--bubble-color: <%= bubble.color %>",
style: "--card-color: <%= card.color %>",
role: "group", aria: { label: "Messages" },
data: {
controller: "created-by-current-user",
+3 -3
View File
@@ -1,6 +1,6 @@
module NotificationsHelper
def notification_title(notification)
title = bubble_title(notification.bubble)
title = card_title(notification.card)
if notification.resource.is_a? Comment
"RE: " + title
@@ -15,7 +15,7 @@ module NotificationsHelper
name = notification.creator.name
case notification_event_action(notification)
when "popped" then "Popped by #{name}"
when "closed" then "Closed by #{name}"
when "published" then "Added by #{name}"
else name
end
@@ -59,6 +59,6 @@ module NotificationsHelper
end
def notification_is_for_initial_assignement?(notification)
notification.event.action == "published" && notification.bubble.assigned_to?(notification.user)
notification.event.action == "published" && notification.card.assigned_to?(notification.user)
end
end
+1 -1
View File
@@ -3,7 +3,7 @@ module TranslationsHelper
user_name: { "🇺🇸": "Enter your name", "🇪🇸": "Introduce tu nombre", "🇫🇷": "Entrez votre nom", "🇮🇳": "अपना नाम दर्ज करें", "🇩🇪": "Geben Sie Ihren Namen ein", "🇧🇷": "Insira seu nome" },
email_address: { "🇺🇸": "Enter your email address", "🇪🇸": "Introduce tu correo electrónico", "🇫🇷": "Entrez votre adresse courriel", "🇮🇳": "अपना ईमेल पता दर्ज करें", "🇩🇪": "Geben Sie Ihre E-Mail-Adresse ein", "🇧🇷": "Insira seu endereço de email" },
password: { "🇺🇸": "Enter your password", "🇪🇸": "Introduce tu contraseña", "🇫🇷": "Saisissez votre mot de passe", "🇮🇳": "अपना पासवर्ड दर्ज करें", "🇩🇪": "Geben Sie Ihr Passwort ein", "🇧🇷": "Insira sua senha" },
bucket_name: { "🇺🇸": "Give it a name", "🇪🇸": "Dale un nombre", "🇫🇷": "Donnez lui un nom", "🇮🇳": "इसे एक नाम दें", "🇩🇪": "Gib ihm einen Namen", "🇧🇷": "Dê-lhe um nome" }
collection_name: { "🇺🇸": "Give it a name", "🇪🇸": "Dale un nombre", "🇫🇷": "Donnez lui un nom", "🇮🇳": "इसे एक नाम दें", "🇩🇪": "Gib ihm einen Namen", "🇧🇷": "Dê-lhe um nome" }
}
def translations_for(translation_key)
+3 -3
View File
@@ -1,7 +1,7 @@
module WorkflowsHelper
def button_to_set_stage(bubble, stage)
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 } ],
def button_to_set_stage(card, stage)
button_to card_stagings_path(card, stage_id: stage),
method: :post, class: [ "btn full-width justify-start workflow-stage txt-uppercase", { "workflow-stage--current": stage == card.stage } ],
form_class: "flex align-center gap-half",
data: { turbo_frame: "_top" } do
tag.span class: "overflow-ellipsis" do
@@ -3,7 +3,7 @@ import { Controller } from "@hotwired/stimulus"
const SIZES = [ "one", "two", "three", "four", "five" ]
export default class extends Controller {
static targets = [ "bubble" ]
static targets = [ "card" ]
connect() {
this.resize()
@@ -12,19 +12,19 @@ export default class extends Controller {
resize() {
const [ min, max ] = this.#getScoreRange()
this.bubbleTargets.forEach(bubble => {
const score = this.#currentBubbleScore(bubble)
this.cardTargets.forEach(card => {
const score = this.#currentCardScore(card)
const idx = Math.round((score - min) / (max - min) * (SIZES.length - 1))
bubble.style.setProperty("--bubble-size", `var(--bubble-size-${SIZES[idx]})`)
card.style.setProperty("--card-size", `var(--card-size-${SIZES[idx]})`)
})
}
#getScoreRange() {
var min = 0, max = 1;
this.bubbleTargets.forEach(bubble => {
const score = this.#currentBubbleScore(bubble)
this.cardTargets.forEach(card => {
const score = this.#currentCardScore(card)
min = Math.min(min, score)
max = Math.max(max, score)
@@ -33,7 +33,7 @@ export default class extends Controller {
return [ min, max ]
}
#currentBubbleScore(el) {
#currentCardScore(el) {
const score = el.dataset.activityScore
const scoreAt = el.dataset.activityScoreAt
const daysAgo = (Date.now() / 1000 - scoreAt) / (60 * 60 * 24)
@@ -47,7 +47,7 @@ export default class extends Controller {
#triggerChangeEvent(newValue) {
if (this.inputTarget.tagName === "HOUSE-MD") {
this.inputTarget.dispatchEvent(new CustomEvent('house-md:change', {
bubbles: true,
cards: true,
detail: {
previousContent: '',
newContent: newValue
@@ -12,7 +12,7 @@ export default class extends Controller {
for (const file of files) {
const uploadEvent = new CustomEvent("house-md:before-upload", {
bubbles: true,
cards: true,
detail: { file },
cancelable: true
})
@@ -1,9 +1,9 @@
class Bubble::AutoPopAllDueJob < ApplicationJob
class Card::AutoCloseAllDueJob < ApplicationJob
queue_as :default
def perform
ApplicationRecord.with_each_tenant do |tenant|
Bubble.auto_pop_all_due
Card.auto_close_all_due
end
end
end
@@ -1,9 +1,9 @@
class Bubble::AutoReconsiderAllStagnatedJob < ApplicationJob
class Card::AutoReconsiderAllStagnatedJob < ApplicationJob
queue_as :default
def perform
ApplicationRecord.with_each_tenant do |tenant|
Bubble.auto_reconsider_all_stagnated
Card.auto_reconsider_all_stagnated
end
end
end
+1 -1
View File
@@ -3,7 +3,7 @@ class RemoveAbandonedCreationsJob < ApplicationJob
def perform
ApplicationRecord.with_each_tenant do |tenant|
Bubble.remove_abandoned_creations
Card.remove_abandoned_creations
end
end
end
+1 -1
View File
@@ -1,5 +1,5 @@
class Access < ApplicationRecord
belongs_to :bucket
belongs_to :collection
belongs_to :user
enum :involvement, %i[ access_only watching everything ].index_by(&:itself)
+3 -3
View File
@@ -1,8 +1,8 @@
class Account < ApplicationRecord
include PopReasons, Joinable
include ClosureReasons, Joinable
has_many :buckets, dependent: :destroy
has_many :bubbles, through: :buckets
has_many :collections, dependent: :destroy
has_many :cards, through: :collections
has_many :users, dependent: :destroy do
def system
@@ -1,4 +1,4 @@
module Account::PopReasons
module Account::ClosureReasons
extend ActiveSupport::Concern
DEFAULT_LABELS = [
@@ -11,19 +11,19 @@ module Account::PopReasons
FALLBACK_LABEL = "Done"
included do
has_many :pop_reasons, dependent: :destroy, class_name: "Pop::Reason" do
has_many :closure_reasons, dependent: :destroy, class_name: "Closure::Reason" do
def labels
pluck(:label).presence || [ FALLBACK_LABEL ]
end
end
after_create :create_default_pop_reasons
after_create :create_default_closure_reasons
end
private
def create_default_pop_reasons
def create_default_closure_reasons
DEFAULT_LABELS.each do |label|
pop_reasons.create! label: label
closure_reasons.create! label: label
end
end
end
+1 -1
View File
@@ -1,5 +1,5 @@
class Assignment < ApplicationRecord
belongs_to :bubble, touch: true
belongs_to :card, touch: true
belongs_to :assignee, class_name: "User"
belongs_to :assigner, class_name: "User"
-3
View File
@@ -1,3 +0,0 @@
class Bubble::Engagement < ApplicationRecord
belongs_to :bubble, class_name: "::Bubble", touch: true
end
-56
View File
@@ -1,56 +0,0 @@
module Bubble::Poppable
extend ActiveSupport::Concern
AUTO_POP_AFTER = 30.days
included do
has_one :pop, dependent: :destroy
scope :popped, -> { joins(:pop) }
scope :active, -> { where.missing(:pop) }
scope :recently_popped_first, -> { popped.order("pops.created_at": :desc) }
scope :due_to_be_popped, -> { considering.where(last_active_at: ..AUTO_POP_AFTER.ago) }
end
class_methods do
def auto_pop_all_due
due_to_be_popped.find_each do |bubble|
bubble.pop!(user: bubble.bucket.account.users.system, reason: "Closed")
end
end
end
def auto_pop_at
last_active_at + AUTO_POP_AFTER if last_active_at
end
def popped?
pop.present?
end
def active?
!popped?
end
def popped_by
pop&.user
end
def popped_at
pop&.created_at
end
def pop!(user: Current.user, reason: Account::PopReasons::FALLBACK_LABEL)
unless popped?
transaction do
create_pop! user: user, reason: reason
track_event :popped, creator: user
end
end
end
def unpop
pop&.destroy
end
end
-21
View File
@@ -1,21 +0,0 @@
class Bucket < ApplicationRecord
include Accessible, Broadcastable, Filterable
belongs_to :account
belongs_to :creator, class_name: "User", default: -> { Current.user }
belongs_to :workflow, optional: true
has_many :bubbles, dependent: :destroy
has_many :tags, -> { distinct }, through: :bubbles
validates_presence_of :name
after_save :update_bubbles_workflow, if: :saved_change_to_workflow_id?
scope :alphabetically, -> { order("lower(name)") }
private
def update_bubbles_workflow
bubbles.update_all(stage_id: workflow&.stages&.first&.id)
end
end
-8
View File
@@ -1,8 +0,0 @@
module Bucket::Broadcastable
extend ActiveSupport::Concern
included do
broadcasts_refreshes
broadcasts_refreshes_to ->(bucket) { [ bucket.account, :buckets ] }
end
end
+7 -7
View File
@@ -1,8 +1,8 @@
class Bubble < ApplicationRecord
class Card < ApplicationRecord
include Assignable, Boostable, Colored, Commentable, Engageable, Eventable,
Messages, Notifiable, Pinnable, Poppable, Scorable, Searchable, Staged, Statuses, Taggable, Watchable
Messages, Notifiable, Pinnable, Closeable, Scorable, Searchable, Staged, Statuses, Taggable, Watchable
belongs_to :bucket, touch: true
belongs_to :collection, touch: true
belongs_to :creator, class_name: "User", default: -> { Current.user }
has_many :notifications, dependent: :destroy
@@ -15,7 +15,7 @@ class Bubble < ApplicationRecord
scope :reverse_chronologically, -> { order created_at: :desc, id: :desc }
scope :chronologically, -> { order created_at: :asc, id: :asc }
scope :in_bucket, ->(bucket) { where bucket: bucket }
scope :in_collection, ->(collection) { where collection: collection }
scope :indexed_by, ->(index) do
case index
@@ -25,12 +25,12 @@ class Bubble < ApplicationRecord
when "newest" then reverse_chronologically
when "oldest" then chronologically
when "stalled" then ordered_by_staleness
when "popped" then popped
when "closed" then closed
end
end
def cache_key
[ super, bucket&.name ].compact.join("/")
[ super, collection&.name ].compact.join("/")
end
private
@@ -56,7 +56,7 @@ class Bubble < ApplicationRecord
end
def assign_initial_stage
if workflow_stage = bucket.workflow&.stages&.first
if workflow_stage = collection.workflow&.stages&.first
self.stage = workflow_stage
save! touch: false
track_event :staged, stage_id: workflow_stage.id, stage_name: workflow_stage.name
@@ -1,4 +1,4 @@
module Bubble::Assignable
module Card::Assignable
extend ActiveSupport::Concern
included do
@@ -1,4 +1,4 @@
module Bubble::Boostable
module Card::Boostable
extend ActiveSupport::Concern
included do
+56
View File
@@ -0,0 +1,56 @@
module Card::Closeable
extend ActiveSupport::Concern
AUTO_CLOSURE_AFTER = 30.days
included do
has_one :closure, dependent: :destroy
scope :closed, -> { joins(:closure) }
scope :active, -> { where.missing(:closure) }
scope :recently_closed_first, -> { closed.order("closures.created_at": :desc) }
scope :due_to_be_closed, -> { considering.where(last_active_at: ..AUTO_CLOSURE_AFTER.ago) }
end
class_methods do
def auto_close_all_due
due_to_be_closed.find_each do |card|
card.close(user: card.collection.account.users.system, reason: "Closed")
end
end
end
def auto_close_at
last_active_at + AUTO_CLOSURE_AFTER if last_active_at
end
def closed?
closure.present?
end
def active?
!closed?
end
def closed_by
closure&.user
end
def closed_at
closure&.created_at
end
def close(user: Current.user, reason: Account::ClosureReasons::FALLBACK_LABEL)
unless closed?
transaction do
create_closure! user: user, reason: reason
track_event :closed, creator: user
end
end
end
def reopen
closure&.destroy
end
end
@@ -1,4 +1,4 @@
module Bubble::Colored
module Card::Colored
extend ActiveSupport::Concern
COLORS = %w[ #b7462b #ff63a8 #eb7a32 #6ac967 #2c6da8 #663251 ]
@@ -1,4 +1,4 @@
module Bubble::Commentable
module Card::Commentable
extend ActiveSupport::Concern
included do
@@ -1,10 +1,10 @@
module Bubble::Engageable
module Card::Engageable
extend ActiveSupport::Concern
STAGNATED_AFTER = 30.days
included do
has_one :engagement, dependent: :destroy, class_name: "Bubble::Engagement"
has_one :engagement, dependent: :destroy, class_name: "Card::Engagement"
scope :doing, -> { published.active.joins(:engagement) }
scope :considering, -> { published.active.where.missing(:engagement) }
@@ -14,8 +14,8 @@ module Bubble::Engageable
class_methods do
def auto_reconsider_all_stagnated
stagnated.find_each do |bubble|
bubble.reconsider
stagnated.find_each do |card|
card.reconsider
end
end
end
@@ -35,7 +35,7 @@ module Bubble::Engageable
def engage
unless doing?
transaction do
unpop
reopen
create_engagement!
end
end
@@ -43,7 +43,7 @@ module Bubble::Engageable
def reconsider
transaction do
unpop
reopen
engagement&.destroy
end
end
+3
View File
@@ -0,0 +1,3 @@
class Card::Engagement < ApplicationRecord
belongs_to :card, class_name: "::Card", touch: true
end
@@ -1,4 +1,4 @@
module Bubble::Eventable
module Card::Eventable
extend ActiveSupport::Concern
included do
@@ -13,7 +13,7 @@ module Bubble::Eventable
private
def track_event(action, creator: Current.user, **particulars)
if published?
event = find_or_capture_event_summary.events.create! action: action, creator: creator, bubble: self, particulars: particulars
event = find_or_capture_event_summary.events.create! action: action, creator: creator, card: self, particulars: particulars
event.generate_notifications_later
end
end
@@ -1,4 +1,4 @@
module Bubble::Messages
module Card::Messages
extend ActiveSupport::Concern
included do
@@ -1,4 +1,4 @@
module Bubble::Pinnable
module Card::Pinnable
extend ActiveSupport::Concern
included do
@@ -1,4 +1,4 @@
module Bubble::Scorable
module Card::Scorable
extend ActiveSupport::Concern
REFERENCE_DATE = Time.utc(2025, 1, 1)
@@ -37,7 +37,7 @@ module Bubble::Scorable
def event_weight(event)
case
when event.boosted? then 1
when event.comment&.first_by_author_on_bubble? then 20
when event.comment&.first_by_author_on_card? then 20
when event.comment&.follows_comment_by_another_author? then 15
when event.commented? then 10
else 0
@@ -46,7 +46,7 @@ module Bubble::Scorable
def event_score_reference(score, activity_at)
# The reference score is used to make the activity score comparable
# across different bubbles, since it represents the bubble's activity
# across different cards, since it represents the card's activity
# level at a consistent point in time.
#
# We store this as log2 to tame the huge/tiny numbers we'd otherwise get
@@ -1,17 +1,17 @@
module Bubble::Searchable
module Card::Searchable
extend ActiveSupport::Concern
included do
include ::Searchable
searchable_by :title, using: :bubbles_search_index
searchable_by :title, using: :cards_search_index
scope :mentioning, ->(query) do
if query = sanitize_query_syntax(query)
bubbles = Bubble.search(query).select(:id).to_sql
cards = Card.search(query).select(:id).to_sql
comments = Comment.search(query).select(:id).to_sql
left_joins(:messages).where("bubbles.id in (#{bubbles}) or messages.messageable_id in (#{comments})").distinct
left_joins(:messages).where("cards.id in (#{cards}) or messages.messageable_id in (#{comments})").distinct
else
none
end
@@ -1,4 +1,4 @@
module Bubble::Staged
module Card::Staged
extend ActiveSupport::Concern
included do
@@ -1,4 +1,4 @@
module Bubble::Statuses
module Card::Statuses
extend ActiveSupport::Concern
included do
@@ -9,7 +9,7 @@ module Bubble::Statuses
class_methods do
def remove_abandoned_creations
Bubble.creating.where(updated_at: ..1.day.ago).destroy_all
Card.creating.where(updated_at: ..1.day.ago).destroy_all
end
end
@@ -18,8 +18,8 @@ module Bubble::Statuses
end
def recover_abandoned_creation
abandoned_creations.last.tap do |bubble|
Bubble.creating.where(creator: creator).excluding(bubble).destroy_all
abandoned_creations.last.tap do |card|
Card.creating.where(creator: creator).excluding(card).destroy_all
end
end
@@ -32,6 +32,6 @@ module Bubble::Statuses
private
def abandoned_creations
Bubble.creating.where(creator: creator).where("created_at != updated_at").excluding(self)
Card.creating.where(creator: creator).where("created_at != updated_at").excluding(self)
end
end
@@ -1,4 +1,4 @@
module Bubble::Taggable
module Card::Taggable
extend ActiveSupport::Concern
included do
@@ -9,7 +9,7 @@ module Bubble::Taggable
end
def toggle_tag_with(title)
tag = bucket.account.tags.find_or_create_by!(title: title)
tag = collection.account.tags.find_or_create_by!(title: title)
transaction do
if tagged_with?(tag)
@@ -1,4 +1,4 @@
module Bubble::Watchable
module Card::Watchable
extend ActiveSupport::Concern
included do
@@ -22,7 +22,7 @@ module Bubble::Watchable
def watchers_and_subscribers(include_only_watching: false)
involvements = include_only_watching ? [ :watching, :everything ] : :everything
subscribers = bucket.users.where(accesses: { involvement: involvements })
subscribers = collection.users.where(accesses: { involvement: involvements })
User.where(id: subscribers.pluck(:id) +
watches.watching.pluck(:user_id) - watches.not_watching.pluck(:user_id))
+4
View File
@@ -0,0 +1,4 @@
class Closure < ApplicationRecord
belongs_to :card, touch: true
belongs_to :user, optional: true
end

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