Merge branch 'main' into collections-filter

* main: (24 commits)
  Move load-balancer to fizzy-lb-101
  Add otel_collector accessory for load balancer
  Fix: this was getting idiomorph confused as it was being invoked on many subtree elements
  Keep column popups during page refreshes
  Don't render next link unless there is more activity
  Live updates for the collection perma
  Keep filter panel during page refreshes
  Add pagination to the list of filtered cards
  Use frame based pagination for the users timeline too
  Use frame-based pagination for the activity timeline
  Don't broadcast updates of cards unless published
  Live updates for the timeline
  Make permanent to avoid losing menu on page refreshes
  Use the new required attribute that now Lexxy supports
  Use controller to disable empty comment submit
  Dynamic height for pins based on viewport height
  Position lexxy language picker
  Replace old link with proper collection path
  No longer needed
  Prevent card bubbles getting clipped in grid layout
  ...
This commit is contained in:
Jason Zimdars
2025-10-02 14:26:45 -05:00
29 changed files with 163 additions and 62 deletions
+1 -1
View File
@@ -359,7 +359,7 @@ GEM
logger (~> 1.6)
letter_opener (1.10.0)
launchy (>= 2.2, < 4)
lexxy (0.1.9.beta)
lexxy (0.1.10.beta)
rails (>= 8.0.2)
lint_roller (1.1.0)
logger (1.7.0)
+1 -1
View File
@@ -73,7 +73,7 @@
z-index: 3;
}
&:not(.cards--considering, .is-collapsed) {
&:not(.cards--considering, .is-collapsed, .cards--grid) {
overflow: clip;
}
+3 -8
View File
@@ -31,12 +31,6 @@
position: absolute;
white-space: pre-line;
}
/* Disable the submit button when Lexical is empty */
+ .comment__submit {
opacity: 0.5;
pointer-events: none;
}
}
}
@@ -178,11 +172,12 @@
border: 1px solid var(--color-ink-lighter);
border-radius: 0.3em;
color: var(--color-ink);
font-family: var(--font-mono);
font-family: var(--font-base);
font-size: var(--text-x-small);
font-weight: 500;
inset-inline-end: 0;
line-height: inherit;
margin: 0.3em 0.3em 0 -0.3em;
margin: 0.3em 0.3em 0 0;
padding: 0.5em 1.8em 0.5em 1.2em;
text-align: start;
+1 -1
View File
@@ -178,7 +178,7 @@
}
.attachment__caption {
color: var(--color-ink-dark);
color: color-mix(in oklch, var(--color-ink) 66%, transparent);
font-size: var(--text-small);
.input {
+24 -1
View File
@@ -345,7 +345,30 @@
}
}
&:nth-child(1n + 11) { display: none; }
/* Show 6 max items on smallest devices */
@media (max-height: 578px) {
&:nth-child(1n + 7) { display: none; }
}
/* 7 max */
@media (min-height: 578px) and (max-height: 656px) {
&:nth-child(1n + 8) { display: none; }
}
/* 8 max */
@media (min-height: 656px) and (max-height: 734px) {
&:nth-child(1n + 9) { display: none; }
}
/* 9 max */
@media (min-height: 734px) and (max-height: 812px) {
&:nth-child(1n + 10) { display: none; }
}
/* 10 max on larger screens */
@media (min-height: 812px) {
&:nth-child(1n + 11) { display: none; }
}
&:not([aria-selected]) .card__link:focus-visible,
.tray__dialog:not([open]) & .card__link:focus-visible {
+11 -1
View File
@@ -27,7 +27,9 @@ class CardsController < ApplicationController
end
def update
@card.update! card_params
suppressing_broadcasts_unless_published(@card) do
@card.update! card_params
end
if @card.published?
render_card_replacement
@@ -50,6 +52,14 @@ class CardsController < ApplicationController
@card = Current.user.accessible_cards.find params[:id]
end
def suppressing_broadcasts_unless_published(card, &block)
if card.published?
yield
else
Collection.suppressing_turbo_broadcasts(&block)
end
end
def card_params
params.expect(card: [ :status, :title, :description, :image, tag_ids: [] ])
end
+9 -1
View File
@@ -24,7 +24,7 @@ class UsersController < ApplicationController
def show
@filter = Current.user.filters.new(creator_ids: [ @user.id ])
@day_timeline = Current.user.timeline_for(Time.current, filter: @filter)
@day_timeline = Current.user.timeline_for(day_param, filter: @filter)
end
def update
@@ -50,6 +50,14 @@ class UsersController < ApplicationController
head :forbidden unless Current.user.can_change?(@user)
end
def day_param
if params[:day].present?
Time.zone.parse(params[:day])
else
Time.current
end
end
def user_params
params.expect(user: [ :name, :email_address, :password, :avatar ])
end
-11
View File
@@ -52,17 +52,6 @@ module EventsHelper
tag.div class: "events__time-block", style: "grid-area: #{row}/#{col}", &
end
def event_next_page_link(next_day)
if next_day
tag.div id: "next_page",
data: { controller: "fetch-on-visible",
fetch_on_visible_url_value: events_days_path(
day: next_day.strftime("%Y-%m-%d"),
**@filter.as_params
) }
end
end
def event_action_sentence(event)
if event.action.comment_created?
comment_event_action_sentence(event)
+24
View File
@@ -35,6 +35,30 @@ module PaginationHelper
end
end
def with_automatic_pagination(name, page, **properties)
pagination_list name, paginate_on_scroll: true, **properties do
concat(pagination_frame_tag(name, page) do
yield
concat link_to_next_page(name, page, activate_when_observed: true)
end)
end
end
def day_timeline_pagination_frame_tag(day_timeline, &)
turbo_frame_tag day_timeline_pagination_frame_id_for(day_timeline.day), data: { timeline_target: "frame" }, role: "presentation", refresh: :morph, &
end
def day_timeline_pagination_frame_id_for(day)
"day-timeline-pagination-contents-#{day.strftime("%Y-%m-%d")}"
end
def day_timeline_pagination_link(day_timeline, filter)
if day_timeline.next_day
link_to "Load more...", events_days_path(day: day_timeline.next_day.strftime("%Y-%m-%d"), **filter.as_params),
data: { frame: day_timeline_pagination_frame_id_for(day_timeline.next_day), pagination_target: "paginationLink" }
end
end
private
def pagination_list(name, tag_element: :div, paginate_on_scroll: false, **properties, &block)
classes = properties.delete(:class)
@@ -14,7 +14,7 @@ export default class extends Controller {
}
preventToggle(event) {
if (event.detail.attributeName === "class") {
if (event.target.hasAttribute("data-collapsible-columns-target") && event.detail.attributeName === "class") {
event.preventDefault()
}
}
+12 -2
View File
@@ -1,8 +1,8 @@
import { Controller } from "@hotwired/stimulus"
import { debounce } from "helpers/timing_helpers";
import { debounce, nextFrame } from "helpers/timing_helpers";
export default class extends Controller {
static targets = [ "cancel" ]
static targets = [ "cancel", "submit" ]
static values = {
debounceTimeout: { type: Number, default: 300 }
@@ -37,6 +37,16 @@ export default class extends Controller {
event.preventDefault()
}
async disableSubmitWhenInvalid(event) {
await nextFrame()
if (this.element.checkValidity()) {
this.submitTarget.removeAttribute("disabled")
} else {
this.submitTarget.toggleAttribute("disabled", true)
}
}
select(event) {
event.target.select()
}
+2 -2
View File
@@ -10,10 +10,10 @@
local_save_key_value: "comment-#{card.id}",
action: "turbo:submit-end->local-save#submit keydown.ctrl+enter->form#submit:prevent keydown.meta+enter->form#submit:prevent keydown.esc->form#cancel:stop" } do |form| %>
<%= form.rich_textarea :body, required: true, placeholder: new_comment_placeholder(card),
data: { local_save_target: "input", action: "lexxy:change->local-save#save turbo:morph-element->local-save#restoreContent" } do %>
data: { local_save_target: "input", action: "lexxy:change->form#disableSubmitWhenInvalid lexxy:change->local-save#save turbo:morph-element->local-save#restoreContent" } do %>
<%= general_prompts(@card.collection) %>
<% end %>
<%= form.button class: "comment__submit btn btn--reversed flex-item-justify-start" do %>
<%= form.button class: "comment__submit btn btn--reversed flex-item-justify-start", data: { form_target: "submit" }, disabled: true do %>
<span>Post this comment</span>
<% end %>
<span data-form-target="cancel" hidden></span>
+3 -1
View File
@@ -27,6 +27,8 @@
<%= turbo_frame_tag :cards_container do %>
<section class="cards cards--grid">
<%= render partial: "cards/display/preview", collection: @page.records, as: :card, locals: { draggable: true }, cached: ->(card) { cacheable_preview_parts_for(card) } %>
<%= with_automatic_pagination :cards_paginated_container, @page do %>
<%= render partial: "cards/display/preview", collection: @page.records, as: :card, locals: { draggable: true }, cached: ->(card) { cacheable_preview_parts_for(card) } %>
<% end %>
</section>
<% end %>
+1 -2
View File
@@ -3,7 +3,7 @@
<% content_for :header do %>
<%= render "filters/menu", user_filtering: @user_filtering %>
<div class="header__actions header__actions--start">
<%= link_to cards_path(collection_ids: [ @collection ]), class: "btn borderless txt-medium", data: { controller: "hotkey", action: "keydown.esc@document->hotkey#click" } do %>
<%= link_to @collection, class: "btn borderless txt-medium", data: { controller: "hotkey", action: "keydown.esc@document->hotkey#click" } do %>
<span class="overflow-ellipsis">
&larr;
<strong><%= @collection.name %></strong>
@@ -38,7 +38,6 @@
</div>
<div class="settings__panel panel shadow center">
<%# render "collections/edit/workflows", collection: @collection %>
<%= render "collections/edit/auto_close", collection: @collection %>
<%= render "collections/edit/publication", collection: @collection %>
<%= render "collections/edit/delete", collection: @collection %>
+2
View File
@@ -1,6 +1,8 @@
<% @page_title = @collection.name %>
<% turbo_exempts_page_from_cache %>
<%= turbo_stream_from @collection %>
<% content_for :head do %>
<%= tag.meta property: "og:title", content: @page_title %>
<%= tag.meta property: "og:description", content: Account.sole.name %>
@@ -1,4 +1,4 @@
<div class="cards__expander-menu" data-controller="dialog" data-action="keydown.esc->dialog#close click@document->dialog#closeOnClickOutside">
<div class="cards__expander-menu" data-controller="dialog" data-action="keydown.esc->dialog#close click@document->dialog#closeOnClickOutside" data-turbo-permanent>
<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>
@@ -1,4 +1,4 @@
<div class="position-relative" data-controller="dialog" data-action="keydown.esc->dialog#close click@document->dialog#closeOnClickOutside">
<div class="position-relative" data-controller="dialog" data-action="keydown.esc->dialog#close click@document->dialog#closeOnClickOutside" data-turbo-permanent>
<button class="btn btn--circle txt-x-small borderless" data-action="click->dialog#open:stop" aria-label="Add a new column, etc.">
<%= icon_tag "add", class: "translucent", "aria-hidden": true %>
</button>
+4
View File
@@ -0,0 +1,4 @@
<%= day_timeline_pagination_frame_tag @day_timeline do %>
<%= render "events/day", day_timeline: @day_timeline %>
<%= day_timeline_pagination_link(@day_timeline, @filter) %>
<% end %>
@@ -1,7 +0,0 @@
<%= turbo_stream.append :activity do %>
<%= render "events/day", day_timeline: @day_timeline %>
<% end %>
<%= turbo_stream.replace :next_page do %>
<%= event_next_page_link(@day_timeline.next_day) %>
<% end %>
+6 -5
View File
@@ -1,7 +1,7 @@
<% @page_title = "Home" %>
<% @header_class = "header--events" %>
<%= turbo_stream_from :activity_summaries %>
<%= render "filters/broadcasts", filter: @filter %>
<% content_for :header do %>
<%= render "filters/menu", user_filtering: @user_filtering %>
@@ -13,9 +13,10 @@
</h1>
<% end %>
<div class="events" id="activity">
<%= turbo_frame_tag :events_container do %>
<%= tag.div id: "activity", class: "events", data: { controller: "pagination", pagination_paginate_on_intersection_value: true } do %>
<%= day_timeline_pagination_frame_tag @day_timeline do %>
<%= render "events/day", day_timeline: @day_timeline %>
<%= day_timeline_pagination_link(@day_timeline, @filter) %>
<% end %>
</div>
<%= event_next_page_link(@day_timeline.next_day) %>
<% end %>
+1 -1
View File
@@ -1,4 +1,4 @@
<nav data-controller="dialog" data-action="keydown.esc->dialog#close click@document->dialog#closeOnClickOutside">
<nav data-controller="dialog" data-action="keydown.esc->dialog#close click@document->dialog#closeOnClickOutside" data-turbo-permanent>
<%= render "filters/menu/button" %>
<%= tag.dialog class: "fizzy-menu filter popup popup--animated panel margin-block-start-half",
+3 -2
View File
@@ -1,5 +1,5 @@
<% cache user_filtering do %>
<%= tag.aside id: "filter-settings",
<%= 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",
@@ -7,7 +7,8 @@
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 } do %>
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",
+1 -1
View File
@@ -1,6 +1,6 @@
<%= tag.button class:"fizzy-menu-trigger input input--select center flex-inline align-center txt-normal",
data: {
action: "click->dialog#open:stop keydown.meta+j@document->hotkey#click keydown.ctrl+j@document->hotkey#click",
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 %>
<svg fill="none" height="282" viewBox="0 0 178 282" width="178" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<path d="m14.1582 85.2218s66.3075-9.7823 75.9322-9.7823c9.6242 0 73.9516 9.7823 73.9516 9.7823-6.535 65.4602-17.171 132.0422-21.661 160.5982 0 0-16.941 11.257-51.3278 11.257s-55.9319-12.654-55.9319-12.654c-4.4901-28.557-14.4184-93.741-20.9631-159.2012z" fill="#d2edf8"/>
@@ -1,5 +1,5 @@
<% if platform.desktop? %>
<footer class="popup__footer">
Press <kbd class="kbd"><%= hotkey_label(["ctrl", "J"]) -%></kbd> anytime to open this, <kbd class="kbd">esc</kbd> to close, <span class="txt-nowrap"><kbd class="kbd">↑</kbd><kbd class="kbd">↓</kbd> to move,</span> <kbd class="kbd">enter</kbd> to navigate.
Press <kbd class="kbd">J</kbd> anytime to open this, <kbd class="kbd">esc</kbd> to close, <kbd class="kbd">↑</kbd><kbd class="kbd">↓</kbd> to move, <kbd class="kbd">enter</kbd> to navigate.
</footer>
<% end %>
@@ -0,0 +1,13 @@
<div class="events margin-block-double" id="activity" data-controller="pagination" data-pagination-paginate-on-intersection-value="true">
<%= day_timeline_pagination_frame_tag day_timeline do %>
<%= render "events/day", day_timeline: day_timeline %>
<% if day_timeline.next_day %>
<%= link_to "Load more...", user_path(user, day: day_timeline.next_day.strftime("%Y-%m-%d"), **filter.as_params),
data: { frame: day_timeline_pagination_frame_id_for(day_timeline.next_day), pagination_target: "paginationLink" } %>
<% end %>
<% end %>
</div>
+1 -4
View File
@@ -50,7 +50,4 @@
<h1 class="font-weight-black txt-large margin-block-double"><%= "What #{ Current.user == @user ? "have you" : "has #{ @user.first_name }" } been up to?" %></h1>
<div class="events margin-block-double" id="activity">
<%= render "events/day", day_timeline: @day_timeline %>
</div>
<%= event_next_page_link(@day_timeline.next_day) %>
<%= render "users/activity_timeline", user: @user, day_timeline: @day_timeline, filter: @filter %>
+12 -2
View File
@@ -39,11 +39,21 @@ accessories:
load-balancer:
image: basecamp/kamal-proxy:lb
roles:
- web
host: fizzy-beta-lb-01
options:
publish:
- 80:80
- 443:443
volumes:
- load-balancer:/home/kamal-proxy/.config/kamal-proxy
otel_collector-load-balancer:
image: otel/opentelemetry-collector-contrib:0.126.0
port: 9394
files:
- config/otel_collector.yml:/etc/otelcol-contrib/config.yaml
volumes:
- /var/run/docker.sock:/var/run/docker.sock
options:
user: 0
host: fizzy-beta-lb-01
+12 -2
View File
@@ -39,11 +39,21 @@ accessories:
load-balancer:
image: basecamp/kamal-proxy:lb
roles:
- web
host: fizzy-lb-101
options:
publish:
- 80:80
- 443:443
volumes:
- load-balancer:/home/kamal-proxy/.config/kamal-proxy
otel_collector-load-balancer:
image: otel/opentelemetry-collector-contrib:0.126.0
port: 9394
files:
- config/otel_collector.yml:/etc/otelcol-contrib/config.yaml
volumes:
- /var/run/docker.sock:/var/run/docker.sock
options:
user: 0
host: fizzy-lb-101
+12 -2
View File
@@ -39,11 +39,21 @@ accessories:
load-balancer:
image: basecamp/kamal-proxy:lb
roles:
- web
host: fizzy-staging-lb-01
options:
publish:
- 80:80
- 443:443
volumes:
- load-balancer:/home/kamal-proxy/.config/kamal-proxy
otel_collector-load-balancer:
image: otel/opentelemetry-collector-contrib:0.126.0
port: 9394
files:
- config/otel_collector.yml:/etc/otelcol-contrib/config.yaml
volumes:
- /var/run/docker.sock:/var/run/docker.sock
options:
user: 0
host: fizzy-staging-lb-01