Merge branch 'main' into btn-dropdown

* main: (21 commits)
  Migration to migrate URLs after renaming bubbles => cards
  Remove migration
  Restore bubbles.css since some styles are used
  Fix issues with styling card__ components
  Remove bubbles.css
  Fix renames that the scripts missed
  Reorganize templates to remove cards/cards redundancy
  Rename bubbles => cards
  Safari sure loves outlines
  Use a number field, and improve focus styles
  Fix boosting via number input
  Embed additional video formats
  Fix that notifications weren't marked as read
  Fix tray item position
  Update House to bring patch for history restoration error
  Tighten up
  Restore additional missing routes removed in c0f3ff46d7
  Retore route removed in c0f3ff46d7
  Fix visual regression in popup menus
  Perma cards should not always be 100% width
  ...
This commit is contained in:
Andy Smith
2025-04-09 14:17:39 -05:00
340 changed files with 3101 additions and 2916 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%;
}
+55 -30
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;
@@ -112,9 +112,12 @@
}
.card__actions-container {
--border-radius: 1em;
background-color: var(--color-bg);
margin-block: calc(var(--block-space) * -1) var(--block-space);
padding: var(--block-space) var(--inline-space-double);
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;
z-index: 0;
@@ -123,24 +126,25 @@
}
.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);
}
}
.card__actions-container--top {
margin: var(--block-space-half) auto calc(var(--block-space) * -2);
padding: var(--block-space) 3ch;
min-inline-size: 36ch;
margin: calc(var(--block-space) * 1) auto calc(var(--block-space) * -0.33);
padding: 0.5em 0.75em;
#header:has(&) {
position: relative;
@@ -148,11 +152,19 @@
}
.switch__input:checked + .switch__btn {
background-color: var(--bubble-color) !important;
background-color: var(--card-color) !important;
}
strong {
flex: 1 0 42%;
text-align: end;
&:last-of-type {
text-align: start;
}
}
}
.cards .card__assignees:not(:has(.avatar)) {
.txt-uppercase {
display: none;
@@ -191,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;
}
@@ -232,11 +244,11 @@
--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;
padding-block: var(--padding-block);
padding-inline: var(--padding-inline);
margin-block: calc(var(--block-space) * -1) calc(var(--block-space) * -0.33);
padding: var(--padding-block) var(--padding-inline);
&:has(.card__star-input:checked) {
.card {
@@ -377,7 +389,7 @@
}
.card__meta {
--border-color: var(--bubble-color);
--border-color: var(--card-color);
--border-size: 1px;
--row-gap: 0.25em;
@@ -393,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);
@@ -405,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 {
@@ -456,9 +468,11 @@
}
.input.boost__input {
--hover-size: 0;
--input-border-radius: 0;
--input-border-size: 0;
--input-padding: 0;
--outline-size: 0;
color: inherit;
font-size: inherit;
@@ -466,17 +480,28 @@
inline-size: min-content;
max-inline-size: 3ch;
min-inline-size: 1ch;
outline: none;
@supports (field-sizing: content) {
field-sizing: content;
max-inline-size: unset;
}
&:focus {
background-color: var(--color-highlight);
}
&::-webkit-outer-spin-button,
&::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
}
.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;
@@ -485,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 "); }
@@ -66,7 +66,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 {
+1 -1
View File
@@ -26,7 +26,7 @@
inline-size: var(--tray-size);
outline: 0;
pointer-events: none;
position: absolute;
position: absolute !important; /* tmp fix; ideally should remove the position-relative class from tray__item */
scale: var(--scale);
transform: translateY(var(--offset));
transform-origin: center;
+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))

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