Merge branch 'main' into disable-column-restore-transition

This commit is contained in:
Jason Zimdars
2025-10-16 13:47:20 -05:00
committed by GitHub
57 changed files with 6350 additions and 162 deletions
+1 -2
View File
@@ -1,8 +1,7 @@
source "https://rubygems.org"
git_source(:bc) { |repo| "https://github.com/basecamp/#{repo}" }
# gem "rails", github: "rails/rails", branch: "main"
gem "rails", github: "flavorjones/rails", branch: "flavorjones/improve-sqlite-url-support"
gem "rails", github: "rails/rails", branch: "main"
# Assets & front end
gem "importmap-rails"
+57 -57
View File
@@ -1,6 +1,6 @@
GIT
remote: https://github.com/basecamp/activerecord-tenanted
revision: f412b25ff0096064a4763dc0c8d21b6593678137
revision: 82eff28e0d2f857f14cf8d9c94eaa6c37cec58bb
specs:
activerecord-tenanted (0.5.0)
activerecord (>= 8.1.beta)
@@ -40,33 +40,33 @@ GIT
zeitwerk (~> 2)
GIT
remote: https://github.com/flavorjones/rails.git
revision: aa964b6012c614909d8524a497e3e79b956b3ade
branch: flavorjones/improve-sqlite-url-support
remote: https://github.com/rails/rails.git
revision: 1ca947883d1975487e14208e8159726654fc20d1
branch: main
specs:
actioncable (8.1.0.beta1)
actionpack (= 8.1.0.beta1)
activesupport (= 8.1.0.beta1)
actioncable (8.2.0.alpha)
actionpack (= 8.2.0.alpha)
activesupport (= 8.2.0.alpha)
nio4r (~> 2.0)
websocket-driver (>= 0.6.1)
zeitwerk (~> 2.6)
actionmailbox (8.1.0.beta1)
actionpack (= 8.1.0.beta1)
activejob (= 8.1.0.beta1)
activerecord (= 8.1.0.beta1)
activestorage (= 8.1.0.beta1)
activesupport (= 8.1.0.beta1)
actionmailbox (8.2.0.alpha)
actionpack (= 8.2.0.alpha)
activejob (= 8.2.0.alpha)
activerecord (= 8.2.0.alpha)
activestorage (= 8.2.0.alpha)
activesupport (= 8.2.0.alpha)
mail (>= 2.8.0)
actionmailer (8.1.0.beta1)
actionpack (= 8.1.0.beta1)
actionview (= 8.1.0.beta1)
activejob (= 8.1.0.beta1)
activesupport (= 8.1.0.beta1)
actionmailer (8.2.0.alpha)
actionpack (= 8.2.0.alpha)
actionview (= 8.2.0.alpha)
activejob (= 8.2.0.alpha)
activesupport (= 8.2.0.alpha)
mail (>= 2.8.0)
rails-dom-testing (~> 2.2)
actionpack (8.1.0.beta1)
actionview (= 8.1.0.beta1)
activesupport (= 8.1.0.beta1)
actionpack (8.2.0.alpha)
actionview (= 8.2.0.alpha)
activesupport (= 8.2.0.alpha)
nokogiri (>= 1.8.5)
rack (>= 2.2.4)
rack-session (>= 1.0.1)
@@ -74,36 +74,36 @@ GIT
rails-dom-testing (~> 2.2)
rails-html-sanitizer (~> 1.6)
useragent (~> 0.16)
actiontext (8.1.0.beta1)
actiontext (8.2.0.alpha)
action_text-trix (~> 2.1.15)
actionpack (= 8.1.0.beta1)
activerecord (= 8.1.0.beta1)
activestorage (= 8.1.0.beta1)
activesupport (= 8.1.0.beta1)
actionpack (= 8.2.0.alpha)
activerecord (= 8.2.0.alpha)
activestorage (= 8.2.0.alpha)
activesupport (= 8.2.0.alpha)
globalid (>= 0.6.0)
nokogiri (>= 1.8.5)
actionview (8.1.0.beta1)
activesupport (= 8.1.0.beta1)
actionview (8.2.0.alpha)
activesupport (= 8.2.0.alpha)
builder (~> 3.1)
erubi (~> 1.11)
rails-dom-testing (~> 2.2)
rails-html-sanitizer (~> 1.6)
activejob (8.1.0.beta1)
activesupport (= 8.1.0.beta1)
activejob (8.2.0.alpha)
activesupport (= 8.2.0.alpha)
globalid (>= 0.3.6)
activemodel (8.1.0.beta1)
activesupport (= 8.1.0.beta1)
activerecord (8.1.0.beta1)
activemodel (= 8.1.0.beta1)
activesupport (= 8.1.0.beta1)
activemodel (8.2.0.alpha)
activesupport (= 8.2.0.alpha)
activerecord (8.2.0.alpha)
activemodel (= 8.2.0.alpha)
activesupport (= 8.2.0.alpha)
timeout (>= 0.4.0)
activestorage (8.1.0.beta1)
actionpack (= 8.1.0.beta1)
activejob (= 8.1.0.beta1)
activerecord (= 8.1.0.beta1)
activesupport (= 8.1.0.beta1)
activestorage (8.2.0.alpha)
actionpack (= 8.2.0.alpha)
activejob (= 8.2.0.alpha)
activerecord (= 8.2.0.alpha)
activesupport (= 8.2.0.alpha)
marcel (~> 1.0)
activesupport (8.1.0.beta1)
activesupport (8.2.0.alpha)
base64
bigdecimal
concurrent-ruby (~> 1.0, >= 1.3.1)
@@ -116,23 +116,23 @@ GIT
securerandom (>= 0.3)
tzinfo (~> 2.0, >= 2.0.5)
uri (>= 0.13.1)
rails (8.1.0.beta1)
actioncable (= 8.1.0.beta1)
actionmailbox (= 8.1.0.beta1)
actionmailer (= 8.1.0.beta1)
actionpack (= 8.1.0.beta1)
actiontext (= 8.1.0.beta1)
actionview (= 8.1.0.beta1)
activejob (= 8.1.0.beta1)
activemodel (= 8.1.0.beta1)
activerecord (= 8.1.0.beta1)
activestorage (= 8.1.0.beta1)
activesupport (= 8.1.0.beta1)
rails (8.2.0.alpha)
actioncable (= 8.2.0.alpha)
actionmailbox (= 8.2.0.alpha)
actionmailer (= 8.2.0.alpha)
actionpack (= 8.2.0.alpha)
actiontext (= 8.2.0.alpha)
actionview (= 8.2.0.alpha)
activejob (= 8.2.0.alpha)
activemodel (= 8.2.0.alpha)
activerecord (= 8.2.0.alpha)
activestorage (= 8.2.0.alpha)
activesupport (= 8.2.0.alpha)
bundler (>= 1.15.0)
railties (= 8.1.0.beta1)
railties (8.1.0.beta1)
actionpack (= 8.1.0.beta1)
activesupport (= 8.1.0.beta1)
railties (= 8.2.0.alpha)
railties (8.2.0.alpha)
actionpack (= 8.2.0.alpha)
activesupport (= 8.2.0.alpha)
irb (~> 1.13)
rackup (>= 1.0.0)
rake (>= 12.2)
+1 -1
View File
@@ -1 +1 @@
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="m.6 22.4c.6.6 1.5.6 2.1 0l5.3-5.3 3 3c.5.4 1.1.7 1.8.7h.3c.7-.1 1.4-.5 1.8-1.2.9-1.7 1.3-3.6 1-5.5l5.9-3.7c1.2-.7 1.5-2.2.8-3.4-.1-.2-.2-.3-.3-.4l-5.8-5.8c-.4-.5-1-.7-1.7-.7s-1.3.2-1.8.7c-.1 0-.2.2-.3.4l-3.7 5.9c-1.9-.3-3.8 0-5.5 1-1.2.7-1.6 2.2-.9 3.4.1.2.2.4.4.5l3 3-5.4 5.4c-.6.6-.6 1.5 0 2zm3.8-11.8c-.1 0-.1-.1-.1-.1v-.4c0-.1.1-.2.2-.3 1.4-.8 3.1-1.1 4.7-.7l.7.2 4.5-7h.1c.1-.2.2-.2.3-.2h.3l5.7 5.8c.2.2.1.5 0 .6l-7 4.5.2.7c.4 1.6.1 3.3-.7 4.7 0 .1-.3.2-.4.2 0 .1-.3 0-.4-.1z"/></svg>
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="m23.5 19.5-6.2-6.2 4.8-3.1c.7-.4.9-1.3.5-2 0 0-.2-.3-.2-.3l-5.8-5.8c-.6-.6-1.5-.6-2.1 0s-.2.3-.2.3l-3.1 4.9-6.8-7c-.4-.3-1-.3-1.4 0-.4.4-.4 1 0 1.4l7 7s0 0 0 0l5.8 5.8s0 0 0 0l6.4 6.4c.4.4 1 .4 1.4 0s.4-1 0-1.4z"/><path d="m4.8 9.6c-.7.4-.9 1.3-.5 2 0 0 .2.3.2.3l3 3s-.1 0-.2.1l-5.9 5.9c-.6.6-.6 1.5 0 2.1s1.5.6 2.1 0l5.9-5.9s0 0 0-.1l3.1 3.1c.3.3.7.4 1.2.4s.9-.3 1.1-.7c.6-1 .9-2 1-3.1l-7.9-8.1c-1 0-2.1.4-3 1z"/></svg>

Before

Width:  |  Height:  |  Size: 558 B

After

Width:  |  Height:  |  Size: 489 B

+3 -2
View File
@@ -3,14 +3,15 @@
--row-gap: 0.2lh;
background-color: var(--color-terminal-bg);
block-size: var(--footer-height);
block-size: calc(var(--footer-height) + env(safe-area-inset-bottom));
color: var(--color-terminal-text);
display: flex;
flex-direction: column;
font-size: 0.9em;
inset: auto 0 0 0;
max-block-size: 100%;
padding: var(--block-space) calc(var(--tray-size) + calc(var(--inline-space) * 3)) calc(var(--block-space) + env(safe-area-inset-bottom));
padding-block: var(--block-space) calc(var(--block-space) + env(safe-area-inset-bottom));
padding-inline: calc(var(--tray-size) + calc(var(--inline-space) * 3));
place-content: center;
position: fixed;
z-index: var(--z-terminal);
+25 -22
View File
@@ -234,6 +234,7 @@
transition: opacity 300ms ease-in-out;
opacity: 0;
&:focus-visible,
.cards:hover & {
opacity: 1;
}
@@ -273,15 +274,17 @@
font-weight: inherit;
font-size: inherit;
line-height: var(--column-width-collapsed);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
.cards.is-collapsed & {
max-inline-size: 50vh;
writing-mode: vertical-rl;
}
.cards:not(.is-collapsed, .cards--considering) & {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-inline-size: calc(100% - var(--column-width-collapsed) * 2);
}
}
@@ -595,6 +598,10 @@
border: 1px solid var(--border-color);
text-align: center;
&:has(dialog[open]) {
z-index: 5;
}
.separator--horizontal {
margin-block: var(--block-space);
}
@@ -621,32 +628,28 @@
}
.collection-tools__watching {
--btn-size: 32px;
--gap: 0.5ch;
display: flex;
gap: 0.5ch;
gap: var(--gap);
inline-size: 100%;
margin-block: var(--block-space-half);
overflow: hidden;
place-content: center;
position: relative;
}
&:before,
&:after {
aspect-ratio: 0.5;
background: linear-gradient(to right, var(--color-canvas), transparent);
content: "";
inset-block: 0;
position: absolute;
z-index: 1;
}
.collection-tools__watching-dialog {
--panel-padding: 2ch;
--panel-size: 100%;
&:before {
inset-inline-start: 0;
}
&:after {
inset-inline-end: 0;
rotate: 180deg;
}
display: flex;
flex-wrap: wrap;
gap: var(--gap);
inset-block-start: 0;
justify-content: center;
position: absolute;
z-index: 1;
}
/* On Deck (Not Now)
+26
View File
@@ -0,0 +1,26 @@
@layer components {
.pagination-link {
display: block;
flex: 1;
min-block-size: 1.5rem;
pointer-events: none;
text-decoration: none;
&.pagination-link--active-when-observed {
block-size: 0;
min-block-size: 0;
font-size: 0;
line-height: 0;
overflow: hidden;
visibility: hidden;
height: 0px;
text-indent: -9999px;
}
&[aria-busy="true"] {
.spinner {
display: block;
}
}
}
}
@@ -3,7 +3,7 @@ class Columns::Cards::Drops::StreamsController < ApplicationController
def create
@card.send_back_to_triage
set_page_and_extract_portion_from @collection.cards.awaiting_triage.reverse_chronologically
set_page_and_extract_portion_from @collection.cards.awaiting_triage.by_last_activity.with_golden_first
render turbo_stream: turbo_stream.replace("the-stream", partial: "collections/show/stream", method: :morph, locals: { collection: @card.collection, page: @page })
end
+10 -3
View File
@@ -3,9 +3,9 @@ class Prompts::CardsController < ApplicationController
def index
@cards = if filter_param.present?
prepending_exact_matches_by_id(published_cards.mentioning(params[:filter]))
prepending_exact_matches_by_id(search_cards)
else
@cards = published_cards.latest
published_cards.latest
end
if stale? etag: @cards
@@ -18,8 +18,15 @@ class Prompts::CardsController < ApplicationController
params[:filter]
end
def search_cards
published_cards
.mentioning(params[:filter])
.reverse_chronologically
.limit(MAX_RESULTS)
end
def published_cards
Current.user.accessible_cards.published.limit(MAX_RESULTS)
Current.user.accessible_cards.published
end
def prepending_exact_matches_by_id(cards)
+6 -1
View File
@@ -35,7 +35,12 @@ module AccessesHelper
tag.div(class: "collection-tools__watching") do
safe_join([
safe_join(displayed_watchers.map { |watcher| avatar_tag(watcher) }),
(tag.span("+#{overflow_count}", class: "overflow-count") if overflow_count > 0)
(tag.div(data: { controller: "dialog", action: "keydown.esc->dialog#close click@document->dialog#closeOnClickOutside" }) do
concat tag.button("+#{overflow_count}", class: "overflow-count btn btn--circle borderless", data: { action: "dialog#open" }, aria: { label: "Show #{overflow_count} more watchers" })
concat(tag.dialog(class: "collection-tools__watching-dialog dialog panel", data: { dialog_target: "dialog" }, aria: { hidden: "true" }) do
safe_join(watchers.map { |watcher| avatar_tag(watcher) })
end)
end if overflow_count > 0)
].compact)
end
end
+1 -1
View File
@@ -14,7 +14,7 @@ module ColumnsHelper
"drag-and-drop-refresh": true,
controller: "frame",
action: "turbo:before-frame-render->frame#morphRender turbo:before-morph-element->frame#morphReload"
options[:refresh] = :morph if src.present?
turbo_frame_tag(id, src: src, data: data, **options, &block)
end
end
+5 -1
View File
@@ -5,13 +5,15 @@ module PaginationHelper
def link_to_next_page(namespace, page, activate_when_observed: false, label: default_pagination_label(activate_when_observed), data: {}, **attributes)
if page.before_last? && !params[:previous]
pagination_link(namespace, page.number + 1, label: label, activate_when_observed: activate_when_observed, data: data, class: "btn txt-small center-block center", **attributes)
attributes[:class] = class_names(attributes[:class], "btn txt-small center-block center": !activate_when_observed)
pagination_link(namespace, page.number + 1, label: label, activate_when_observed: activate_when_observed, data: data, **attributes)
end
end
def pagination_link(namespace, page_number, activate_when_observed: false, label: default_pagination_label(activate_when_observed), url_params: {}, data: {}, **attributes)
link_to label, url_for(params.permit!.to_h.merge(page: page_number, **url_params)),
"aria-label": "Load page #{page_number}",
id: "#{namespace}-pagination-link-#{page_number}",
class: class_names(attributes.delete(:class), "pagination-link", { "pagination-link--active-when-observed" => activate_when_observed }),
data: {
frame: pagination_frame_id_for(namespace, page_number),
@@ -62,9 +64,11 @@ module PaginationHelper
private
def pagination_list(name, tag_element: :div, paginate_on_scroll: false, **properties, &block)
classes = properties.delete(:class)
properties[:id] ||= "#{name}-pagination-list"
tag.public_send tag_element,
class: token_list(name, "display-contents", classes),
data: { controller: "pagination", pagination_paginate_on_intersection_value: paginate_on_scroll },
**properties,
&block
end
@@ -4,6 +4,9 @@ import { nextFrame } from "helpers/timing_helpers";
export default class extends Controller {
static classes = [ "collapsed", "noTransitions" ]
static targets = [ "column", "button" ]
static values = {
collection: String
}
async connect() {
this.#disableTransitions()
@@ -84,6 +87,6 @@ export default class extends Controller {
}
#localStorageKeyFor(column) {
return `expand-${column.getAttribute("id")}`
return `expand-${this.collectionValue}-${column.getAttribute("id")}`
}
}
@@ -43,4 +43,11 @@ export default class extends Controller {
closeOnClickOutside({ target }) {
if (!this.element.contains(target)) this.close()
}
preventCloseOnMorphing(event) {
if (event.detail?.attributeName === "open") {
event.preventDefault()
event.stopPropagation()
}
}
}
+12 -1
View File
@@ -2,7 +2,7 @@ import { Controller } from "@hotwired/stimulus"
import { debounce, nextFrame } from "helpers/timing_helpers";
export default class extends Controller {
static targets = [ "cancel", "submit" ]
static targets = [ "cancel", "submit", "input" ]
static values = {
debounceTimeout: { type: Number, default: 300 }
@@ -16,6 +16,17 @@ export default class extends Controller {
this.element.requestSubmit()
}
preventEmptySubmit(event) {
const input = this.hasInputTarget ? this.inputTarget : null
if (input) {
const value = (input.value || "").trim()
if (value.length === 0) {
event.preventDefault()
}
}
}
debouncedSubmit(event) {
this.submit(event)
}
@@ -90,7 +90,6 @@ export default class extends Controller {
#replacePaginationLinkWithFrame(linkElement) {
const turboFrame = this.#buildTurboFrameFor(linkElement)
this.#insertTurboFrameAtPosition(linkElement, turboFrame)
linkElement.remove()
}
#buildTurboFrameFor(linkElement) {
+5 -3
View File
@@ -36,11 +36,13 @@ class Notification::Bundle < ApplicationRecord
end
def deliver
processing!
user.in_time_zone do
processing!
Notification::BundleMailer.notification(self).deliver if deliverable?
Notification::BundleMailer.notification(self).deliver if deliverable?
delivered!
delivered!
end
end
def deliver_later
+3 -1
View File
@@ -23,6 +23,7 @@ class Search
"null as comment_body",
"collections.name as collection_name",
"cards.creator_id",
"cards.created_at",
"bm25(cards_search_index, 10.0, 2.0) AS score"
].join(","))
@@ -35,10 +36,11 @@ class Search
"snippet(comments_search_index, 0, '#{HIGHLIGHT_OPENING_MARK}', '#{HIGHLIGHT_CLOSING_MARK}', '...', 20) AS comment_body",
"collections.name as collection_name",
"comments.creator_id",
"comments.created_at",
"bm25(comments_search_index, 1.0) AS score"
].join(","))
union_sql = "(#{cards.to_sql} UNION #{comments.to_sql}) as search_results"
Search::Result.from(union_sql).order(score: :desc)
Search::Result.from(union_sql).order(created_at: :desc)
end
end
+20 -1
View File
@@ -50,18 +50,37 @@ class User::DayTimeline
end
private
TIMELINEABLE_ACTIONS = %w[
card_assigned
card_unassigned
card_published
card_closed
card_reopened
card_due_date_added
card_due_date_changed
card_due_date_removed
card_collection_changed
comment_created
]
def first_day_with_activity_this_week?
day.monday? || (earliest_time.present? && earliest_time < day.beginning_of_week(:monday))
end
def filtered_events
@filtered_events ||= begin
events = Event.where(collection: collections)
events = timelineable_events
events = events.where(creator_id: filter.creators.ids) if filter.creators.present?
events
end
end
def timelineable_events
Event
.where(collection: collections)
.where(action: TIMELINEABLE_ACTIONS)
end
def collections
filter.collections.presence || user.collections
end
-1
View File
@@ -15,7 +15,6 @@ class Webhook < ApplicationRecord
card_due_date_removed
card_published
card_reopened
card_title_changed
card_unassigned
card_unstaged
comment_created
@@ -1,5 +1,5 @@
<div class="settings__panel settings__panel--entropy panel shadow center">
<h2 class="divider txt-large">Sink or Swim</h2>
<p class="margin-none-block-start">Choose default settings for this account. You can override them in individual collections.</p>
<h2 class="divider txt-large">Auto close</h2>
<p class="margin-none-block-start">Fizzy doesnt let stale cards stick around forever. Cards automatically close as “Not now” without activity for specific period of time. <em>This is the default, global setting — you can override it in each collection.</em></p>
<%= render "entropy/auto_close", model: account.default_entropy_configuration, url: account_entropy_configuration_path %>
</div>
@@ -3,7 +3,10 @@
<div class="flex flex-column flex-item-grow max-inline-size">
<header class="card__header">
<div class="card__collection flex align-start">
<span class="card__id"><%= card.id %></span>
<span class="card__id">
<span class="for-screen-reader">Card number</span>
<%= card.id %>
</span>
<span class="card__collection-name">
<span class="overflow-ellipsis"><%= card.collection.name %></span>
</span>
@@ -1,5 +1,8 @@
<label class="card__collection">
<span class="card__id"><%= card.id %></span>
<span class="card__id">
<span class="for-screen-reader">Card number</span>
<%= card.id %>
</span>
<span class="card__collection-name">
<span class="overflow-ellipsis"><%= card.collection.name %></span>
</span>
@@ -3,8 +3,8 @@
<li class="step">
<input type="checkbox" class="step__checkbox" disabled>
<%= form_with model: [card, Step.new], url: card_steps_path(card) do |form| %>
<%= form.text_field :content, class: "input step__content hide-focus-ring", placeholder: "Add a step…", required: true, autocomplete: "off" %>
<%= form_with model: [card, Step.new], url: card_steps_path(card), data: { controller: "form", action: "submit->form#preventEmptySubmit" } do |form| %>
<%= form.text_field :content, class: "input step__content hide-focus-ring", placeholder: "Add a step…", autocomplete: "off", data: { form_target: "input" }, aria: { label: "Add a step" } %>
<% end %>
</li>
</ol>
@@ -7,18 +7,18 @@
} do %>
<svg viewBox="0 0 200 100">
<path id="top-half" fill="transparent" d="M 20,100 A 80,80 0 0,1 180,100"/>
<path id="<%= dom_id(card, "bubble-top-half") %>" fill="transparent" d="M 20,100 A 80,80 0 0,1 180,100"/>
<text text-anchor="middle" fill="currentColor">
<textPath href="#top-half" startOffset="50%" dominant-baseline="middle" data-bubble-target="top"></textPath>
<textPath href="#<%= dom_id(card, "bubble-top-half") %>" startOffset="50%" dominant-baseline="middle" data-bubble-target="top"></textPath>
</text>
</svg>
<span class="bubble__number" data-bubble-target="center"></span>
<svg viewBox="0 0 200 100">
<path id="bottom-half" d="M 20,0 A 80,80 0 0,0 180,0" fill="transparent"/>
<path id="<%= dom_id(card, "bubble-bottom-half") %>" d="M 20,0 A 80,80 0 0,0 180,0" fill="transparent"/>
<text text-anchor="middle" fill="currentColor">
<textPath href="#bottom-half" startOffset="50%" dominant-baseline="middle" data-bubble-target="bottom"></textPath>
<textPath href="#<%= dom_id(card, "bubble-bottom-half") %>" startOffset="50%" dominant-baseline="middle" data-bubble-target="bottom"></textPath>
</text>
</svg>
<% end %>
@@ -13,12 +13,12 @@
</span>
<span class="card__meta-text card__meta-text--updated overflow-ellipsis">
<%= icon_tag "refresh--meta" %>
<%= icon_tag "refresh--meta", aria: { label: "Last updated" } %>
<%= local_datetime_tag(card.last_active_at, style: :daysago) %>
</span>
<span class="card__meta-text card__meta-text--assignees overflow-ellipsis">
<%= icon_tag "arrow-right" if card.assignees.any? %>
<%= icon_tag("arrow-right", aria: { label: "Assigned to" }) if card.assignees.any? %>
<%= card.assignees.map { |assignee| h assignee.familiar_name }.to_sentence(two_words_connector: " / ", last_word_connector: " / ").html_safe %>
</span>
@@ -1 +1 @@
<%= turbo_stream.replace(dom_id(@column), partial: "collections/show/column", method: :morph, locals: { column: @column }) %>
<%= turbo_stream.replace(dom_id(@column), partial: "collections/show/column", method: :morph, locals: { column: @column }) %>
@@ -1,7 +1,7 @@
<%= turbo_frame_tag @collection, :entropy_configuration do %>
<div>
<h2 class="divider txt-large">Sink or Swim</h2>
<p class="margin-none-block-start">Cards automatically move to "Not now" after…</p>
<div class="margin-block-end">
<h2 class="divider txt-large">Auto close</h2>
<p class="margin-none-block-start">Fizzy doesnt let stale cards stick around forever. Cards automatically close as “Not now” if no one updates, comments, or moves a card for…</p>
<%= render "entropy/auto_close", model: collection, url: collection_entropy_configuration_path(collection) %>
</div>
<% end %>
+1 -1
View File
@@ -8,7 +8,7 @@
dragenter->drag-and-strum#dragEnter
drop->drag-and-drop#drop
dragend->drag-and-drop#dragEnd" } do %>
<div class="card-columns" data-controller="collapsible-columns" data-collapsible-columns-collapsed-class="is-collapsed" data-collapsible-columns-no-transitions-class="no-transitions">
<div class="card-columns" data-controller="collapsible-columns" data-collapsible-columns-collection-value="<%= collection.id %>" data-collapsible-columns-collapsed-class="is-collapsed" data-collapsible-columns-no-transitions-class="no-transitions">
<div class="card-columns__left">
<%= render "collections/show/not_now", collection: collection %>
</div>
+1 -1
View File
@@ -6,7 +6,7 @@
<header class="cards__expander">
<h2 class="cards__expander-title position-relative">The Stream</h2>
<%= link_to collection_columns_stream_path(collection), class: "btn btn--circle txt-x-small borderless cards__expander-button", data: { turbo_frame: "_top" } do %>
<%= link_to collection_columns_stream_path(collection), class: "cards__expander-button btn btn--circle txt-x-small borderless", data: { turbo_frame: "_top" } do %>
<%= icon_tag "expand", class: "translucent" %>
<span class="for-screen-reader">Expand column</span>
<% end %>
@@ -1,4 +1,4 @@
<div class="cards__expander-menu" data-controller="dialog" data-action="keydown.esc->dialog#close click@document->dialog#closeOnClickOutside" data-turbo-permanent>
<div id="<%= dom_id(column, :menu) %>" class="cards__expander-menu" data-controller="dialog" data-action="keydown.esc->dialog#close click@document->dialog#closeOnClickOutside">
<button class="btn btn--circle txt-x-small borderless" data-action="click->dialog#open">
<%= icon_tag "menu-dots-horizontal", class: "translucent" %>
<span class="for-screen-reader">Column options</span>
@@ -26,7 +26,7 @@
<div data-controller="dialog" data-action="keydown.esc->dialog#close click@document->dialog#closeOnClickOutside">
<button id="<%= dom_id(column, :manage_menu) %>" class="popup__btn btn" data-action="click->dialog#open:stop" data-clicker-target="clickable" hidden aria-hidden>Save changes</button>
<dialog class="popup panel flex-column gap-half fill-white shadow txt-small margin-block-double" data-dialog-target="dialog">
<dialog class="popup panel flex-column gap-half fill-white shadow txt-small margin-block-double" data-dialog-target="dialog" data-action="turbo:before-morph-attribute->dialog#preventCloseOnMorphing">
<%= render "collections/show/menu/column_form", collection: column.collection, column: column, label: "Save changes" %>
</dialog>
</div>
@@ -1,6 +1,7 @@
<%= form_with model: [collection, column], data: { controller: "form", action: "turbo:submit-end->dialog#close turbo:submit-end->form#reset" } do |form| %>
<%= form.text_field :name, class: "input", placeholder: "Name this column", value: column.name,
required: true, autocomplete: "off", pattern: ".*\\S.*", title: "Column name cannot be blank", data: { action: "focus->form#select" } %>
<div class="color-picker__colors">
<% ([Card::DEFAULT_COLOR] + Card::COLORS).each do |color| %>
<label class="btn txt-small borderless" style="--btn-background: <%= color %>" title="<%= color %>">
@@ -1,4 +1,4 @@
<%= link_to column_path, class: "btn btn--circle txt-x-small borderless cards__expander-button", data: { turbo_frame: "_top" } do %>
<%= link_to column_path, class: "cards__expander-button btn btn--circle txt-x-small borderless", data: { turbo_frame: "_top" } do %>
<%= icon_tag "expand", class: "translucent" %>
<span class="for-screen-reader">Expand column</span>
<% end %>
+5 -1
View File
@@ -1,4 +1,5 @@
<%= link_to event.notifiable_target,
id: dom_id(event, "timelined"),
class: "event event--#{ event.action } #{ "golden-effect" if event.card.golden? } center-block flex flex-column full-width align-start justify-start position-relative",
style: "--card-color: #{ card.closed? ? "var(--color-card-complete)" : card.color };",
target: "_top",
@@ -8,7 +9,10 @@
action: "mouseover->related-element#highlight mouseout->related-element#unhighlight" } do %>
<div class="card__header flex align-start gap full-width">
<h4 class="card__collection flex-inline align-start">
<span class="card__id"><%= card.id %></span>
<span class="card__id">
<span class="for-screen-reader">Card number</span>
<%= card.id %>
</span>
<span class="card__collection-name">
<span class="overflow-ellipsis"><%= event.collection.name %></span>
</span>
+2 -2
View File
@@ -10,7 +10,7 @@
filter_show: user_filtering.show_collections?,
multi_selection_combobox_no_selection_label_value: user_filtering.selected_collections_label } do %>
<button type="button" class="btn borderless btn--plain events__filter-select" data-action="click->dialog#toggle:stop">
<span class="overflow-ellipsis" data-multi-selection-combobox-target="label">
<span data-multi-selection-combobox-target="label">
</span>
</button>
@@ -22,7 +22,7 @@
<strong class="popup__title">Collection…</strong>
<% if user_filtering.collections.many? %>
<%= text_field_tag nil, nil, placeholder: "Filter…", class: "input input--transparent txt-small font-weight-normal", autofocus: true,
<%= text_field_tag nil, nil, id: nil, placeholder: "Filter…", class: "input input--transparent txt-small font-weight-normal", autofocus: true,
type: "search", autocorrect: "off", autocomplete: "off", data: { "1p-ignore": "true", filter_target: "input", action: "input->filter#filter" } %>
<% end %>
+7 -7
View File
@@ -1,13 +1,13 @@
<%= tag.aside \
class: class_names("filters flex align-center gap-half justify-center center margin-block-end", { "filters--expanded": user_filtering.expanded? }),
data: {
controller: "toggle-class filter-settings",
toggle_class_toggle_class: "filters--expanded",
filter_settings_filters_set_class: "filters--has-filters-set",
filter_settings_no_filtering_url_value: no_filtering_url,
filter_settings_refresh_url_value: settings_refresh_path,
filter_settings_cards_url_value: cards_path,
turbo_permanent: true } do %>
controller: "toggle-class filter-settings",
toggle_class_toggle_class: "filters--expanded",
filter_settings_filters_set_class: "filters--has-filters-set",
filter_settings_no_filtering_url_value: no_filtering_url,
filter_settings_refresh_url_value: settings_refresh_path,
filter_settings_cards_url_value: cards_path,
turbo_permanent: true } do %>
<%= form_with url: filter_url, method: :get, class: "", data: {
controller: "form",
turbo_frame: "cards_container",
@@ -0,0 +1,5 @@
<%= collapsible_nav_section "Accounts" do %>
<% Current.user.identity.memberships.where.not(user_tenant: Current.user.tenant).each do |membership| %>
<%= filter_place_menu_item root_url(script_name: "/#{membership.user_tenant}"), "#{membership.account_name}", "logo-color", new_window: true %>
<% end %>
<% end %>
+1 -1
View File
@@ -3,5 +3,5 @@
action: "click->dialog#open:stop keydown.j@document->hotkey#click keydown.meta+j@document->hotkey#click keydown.ctrl+j@document->hotkey#click",
controller: "hotkey" } do %>
<%= inline_svg "fizzy-logo" %>
<strong class="txt-medium overflow-ellipsis margin-none"><%= Account.sole.name %></strong>
<strong class="txt-medium overflow-ellipsis margin-none"><%= Current.user.identity.memberships.many? ? Account.sole.name : "Fizzy" %></strong>
<% end %>
@@ -1,7 +1,6 @@
<% # Using method: :get inside Turbo frame triggers some special Turbo handling where the script_name is lost %>
<% if user_filtering.collections.any? %>
<%= collapsible_nav_section "Collections" do %>
<%= render "filters/menu/collections/all_option", user_filtering: user_filtering %>
<%= render partial: "filters/menu/collections/collection", collection: user_filtering.collections, as: :collection %>
<% end %>
<% end %>
-3
View File
@@ -3,7 +3,4 @@
<%= filter_place_menu_item user_path(Current.user), "My Profile", "person" %>
<%= filter_place_menu_item notifications_path, "Notifications", "bell" %>
<%= filter_place_menu_item notifications_settings_path, "Notification Settings", "settings" %>
<% Current.user.identity.memberships.where.not(user_tenant: Current.user.tenant).each do |membership| %>
<%= filter_place_menu_item root_url(script_name: "/#{membership.user_tenant}"), "#{membership.account_name}", "logo-color", new_window: true %>
<% end %>
<% end %>
@@ -1,8 +0,0 @@
<% if Current.user.collections.many? %>
<li class="popup__item" data-filter-target="item" data-navigable-list-target="item" id="filter-collection-all">
<%= icon_tag "collection", class: "popup__icon" %>
<%= link_to cards_path, class: "popup__btn btn" do %>
<span class="overflow-ellipsis">All collections</span>
<% end %>
</li>
<% end %>
@@ -19,7 +19,7 @@
<strong class="popup__title">Assigned to…</strong>
<% if User.active.many? %>
<%= text_field_tag nil, nil, placeholder: "Filter…", class: "input input--transparent txt-small", autofocus: true,
<%= text_field_tag nil, nil, id: nil, placeholder: "Filter…", class: "input input--transparent txt-small", autofocus: true,
type: "search", autocorrect: "off", autocomplete: "off", data: { "1p-ignore": "true", filter_target: "input", action: "input->filter#filter" } %>
<% end %>
+1 -1
View File
@@ -20,7 +20,7 @@
<strong class="popup__title">Closed by…</strong>
<% if user_filtering.users.many? %>
<%= text_field_tag nil, nil, placeholder: "Filter…", class: "input input--transparent txt-small", autofocus: true,
<%= text_field_tag nil, nil, id: nil, placeholder: "Filter…", class: "input input--transparent txt-small", autofocus: true,
type: "search", autocorrect: "off", autocomplete: "off", data: { "1p-ignore": "true", filter_target: "input", action: "input->filter#filter" } %>
<% end %>
@@ -20,7 +20,7 @@
<strong class="popup__title">Collection…</strong>
<% if user_filtering.collections.many? %>
<%= text_field_tag nil, nil, placeholder: "Filter…", class: "input input--transparent txt-small", autofocus: true,
<%= text_field_tag nil, nil, id: nil, placeholder: "Filter…", class: "input input--transparent txt-small", autofocus: true,
type: "search", autocorrect: "off", autocomplete: "off", data: { "1p-ignore": "true", filter_target: "input", action: "input->filter#filter" } %>
<% end %>
@@ -20,7 +20,7 @@
<strong class="popup__title">Added by…</strong>
<% if user_filtering.users.many? %>
<%= text_field_tag nil, nil, placeholder: "Filter…", class: "input input--transparent txt-small", autofocus: true,
<%= text_field_tag nil, nil, id: nil, placeholder: "Filter…", class: "input input--transparent txt-small", autofocus: true,
type: "search", autocorrect: "off", autocomplete: "off", data: { "1p-ignore": "true", filter_target: "input", action: "input->filter#filter" } %>
<% end %>
+1 -1
View File
@@ -19,7 +19,7 @@
<strong class="popup__title">Tagged…</strong>
<% if user_filtering.tags.many? %>
<%= text_field_tag nil, nil, placeholder: "Filter…", class: "input input--transparent txt-small", autofocus: true,
<%= text_field_tag nil, nil, id: nil, placeholder: "Filter…", class: "input input--transparent txt-small", autofocus: true,
type: "search", autocorrect: "off", autocomplete: "off", data: { "1p-ignore": "true", filter_target: "input", action: "input->filter#filter" } %>
<% end %>
+1
View File
@@ -27,6 +27,7 @@
<%= render "filters/menu/tags", user_filtering: @user_filtering %>
<%= render "filters/menu/users", user_filtering: @user_filtering %>
<%= render "filters/menu/places", user_filtering: @user_filtering %>
<%= render "filters/menu/accounts", user_filtering: @user_filtering %>
<%= render "filters/menu/shortcut_notice" %>
<% end %>
@@ -1,5 +1,8 @@
<div class="card__collection">
<span class="card__id"><%= notification.card.id %></span>
<span class="card__id">
<span class="for-screen-reader">Card number</span>
<%= notification.card.id %>
</span>
<span class="card__collection-name">
<span class="overflow-ellipsis"><%= notification.card.collection.name %></span>
</span>
@@ -1,5 +1,8 @@
<div class="card__collection">
<span class="card__id"><%= notification.card.id %></span>
<span class="card__id">
<span class="for-screen-reader">Card number</span>
<%= notification.card.id %>
</span>
<span class="card__collection-name">
<span class="overflow-ellipsis"><%= notification.card.collection.name %></span>
</span>
@@ -1,5 +1,5 @@
<%= turbo_frame_tag :cards_container do %>
<div class="card-columns" data-controller="collapsible-columns" data-collapsible-columns-collapsed-class="is-collapsed">
<div class="card-columns" data-controller="collapsible-columns" data-collapsible-columns-collection-value="<%= collection.id %>" data-collapsible-columns-collapsed-class="is-collapsed">
<div class="card-columns__left">
<%= render "public/collections/show/not_now", collection: collection %>
@@ -12,4 +12,4 @@
<%= render "public/collections/show/closed", collection: collection %>
</div>
</div>
<% end %>
<% end %>
+1 -1
View File
@@ -1,7 +1,7 @@
# Pin npm packages by running ./bin/importmap
pin "application"
pin "@hotwired/turbo-rails", to: "turbo.min.js"
pin "@hotwired/turbo-rails", to: "turbo.patched.js"
pin "@hotwired/stimulus", to: "stimulus.min.js"
pin "@hotwired/stimulus-loading", to: "stimulus-loading.js"
pin "@rails/request.js", to: "@rails--request.js" # @0.0.11
+1 -1
View File
@@ -4,7 +4,7 @@ default: &default
batch_size: 500
workers:
- queues: [ "default", "solid_queue_recurring", "backend", "webhooks" ]
threads: 1
threads: 3
processes: <%= ENV.fetch("JOB_CONCURRENCY", Concurrent.physical_processor_count) %>
polling_interval: 0.1
+5 -5
View File
@@ -10,13 +10,13 @@ production: &production
# Application cleanup
auto_postpone_all_due:
class: Card::AutoPostponeAllDueJob
schedule: every hour
schedule: every hour at minute 50
remove_abandoned_creations:
class: RemoveAbandonedCreationsJob
schedule: every hour
schedule: every hour at minute 59
delete_unused_tags:
class: DeleteUnusedTagsJob
schedule: every day at 04:00
schedule: every day at 04:02
# Operations cleanup and backups
clear_solid_queue_finished_jobs:
@@ -27,10 +27,10 @@ production: &production
schedule: every hour at minute 52
cleanup_webhook_deliveries:
class: Webhook::CleanupDeliveriesJob
schedule: every 4 hours
schedule: every 4 hours at minute 51
sqlite_backups:
class: SQLiteBackupsJob
schedule: every day at 05:00
schedule: every day at 05:02
# Metrics
yabeda_actioncable:
@@ -0,0 +1,5 @@
class AddIndexToEventsOnCollectionIdAndActionAndCreatedAt < ActiveRecord::Migration[8.1]
def change
add_index :events, [ :collection_id, :action, :created_at ], name: "index_events_on_collection_id_and_action_and_created_at"
end
end
Generated
+2 -1
View File
@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[8.1].define(version: 2025_10_15_123003) do
ActiveRecord::Schema[8.2].define(version: 2025_10_16_153034) do
create_table "accesses", force: :cascade do |t|
t.datetime "accessed_at"
t.integer "collection_id", null: false
@@ -272,6 +272,7 @@ ActiveRecord::Schema[8.1].define(version: 2025_10_15_123003) do
t.json "particulars", default: {}
t.datetime "updated_at", null: false
t.index ["action"], name: "index_events_on_summary_id_and_action"
t.index ["collection_id", "action", "created_at"], name: "index_events_on_collection_id_and_action_and_created_at"
t.index ["collection_id"], name: "index_events_on_collection_id"
t.index ["creator_id"], name: "index_events_on_creator_id"
t.index ["eventable_type", "eventable_id"], name: "index_events_on_eventable"
+19 -1
View File
@@ -2351,6 +2351,24 @@ indexes:
nulls_not_distinct:
comment:
valid: true
- !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition
table: events
name: index_events_on_collection_id_and_action_and_created_at
unique: false
columns:
- collection_id
- action
- created_at
lengths: {}
orders: {}
opclasses: {}
where:
type:
using:
include:
nulls_not_distinct:
comment:
valid: true
- !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition
table: events
name: index_events_on_creator_id
@@ -3197,4 +3215,4 @@ indexes:
nulls_not_distinct:
comment:
valid: true
version: 20251015123003
version: 20251016153034
+16
View File
@@ -114,4 +114,20 @@ class Notification::BundleTest < ActiveSupport::TestCase
bundle.reload
assert bundle.pending?
end
test "deliver sends email with time in user's time zone" do
@user.settings.update!(timezone_name: "Madrid")
freeze_time Time.utc(2025, 1, 15, 14, 30, 0) do
@user.notifications.create!(source: events(:logo_published), creator: @user)
bundle = @user.notification_bundles.pending.last
bundle.deliver
email = ActionMailer::Base.deliveries.last
assert_not_nil email
# Time in Madrid should be 15:30 (UTC+1 in winter)
assert_match /everything since 3pm/i, email.text_part&.body&.to_s
end
end
end
+6050
View File
File diff suppressed because it is too large Load Diff