diff --git a/Gemfile b/Gemfile index 578fa9316..d5dc4384d 100644 --- a/Gemfile +++ b/Gemfile @@ -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" diff --git a/Gemfile.lock b/Gemfile.lock index 2463a90b5..86b4776ae 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -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) diff --git a/app/assets/images/unpinned.svg b/app/assets/images/unpinned.svg index 37313eb27..993bbf75d 100644 --- a/app/assets/images/unpinned.svg +++ b/app/assets/images/unpinned.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/assets/stylesheets/bar.css b/app/assets/stylesheets/bar.css index 23376ce97..79c85c634 100644 --- a/app/assets/stylesheets/bar.css +++ b/app/assets/stylesheets/bar.css @@ -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); diff --git a/app/assets/stylesheets/card-columns.css b/app/assets/stylesheets/card-columns.css index 77749faa1..43e2504b8 100644 --- a/app/assets/stylesheets/card-columns.css +++ b/app/assets/stylesheets/card-columns.css @@ -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) diff --git a/app/assets/stylesheets/pagination.css b/app/assets/stylesheets/pagination.css new file mode 100644 index 000000000..d0fc4ad16 --- /dev/null +++ b/app/assets/stylesheets/pagination.css @@ -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; + } + } + } +} diff --git a/app/controllers/columns/cards/drops/streams_controller.rb b/app/controllers/columns/cards/drops/streams_controller.rb index d9141af7a..f504435e6 100644 --- a/app/controllers/columns/cards/drops/streams_controller.rb +++ b/app/controllers/columns/cards/drops/streams_controller.rb @@ -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 diff --git a/app/controllers/prompts/cards_controller.rb b/app/controllers/prompts/cards_controller.rb index e20ae39a2..77a64178f 100644 --- a/app/controllers/prompts/cards_controller.rb +++ b/app/controllers/prompts/cards_controller.rb @@ -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) diff --git a/app/helpers/accesses_helper.rb b/app/helpers/accesses_helper.rb index 9e265cb23..103df67bb 100644 --- a/app/helpers/accesses_helper.rb +++ b/app/helpers/accesses_helper.rb @@ -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 diff --git a/app/helpers/columns_helper.rb b/app/helpers/columns_helper.rb index 402124455..793103aa7 100644 --- a/app/helpers/columns_helper.rb +++ b/app/helpers/columns_helper.rb @@ -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 diff --git a/app/helpers/pagination_helper.rb b/app/helpers/pagination_helper.rb index f492b411e..b69e9b02b 100644 --- a/app/helpers/pagination_helper.rb +++ b/app/helpers/pagination_helper.rb @@ -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 diff --git a/app/javascript/controllers/collapsible_columns_controller.js b/app/javascript/controllers/collapsible_columns_controller.js index fc64ba414..01289baf0 100644 --- a/app/javascript/controllers/collapsible_columns_controller.js +++ b/app/javascript/controllers/collapsible_columns_controller.js @@ -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")}` } } diff --git a/app/javascript/controllers/dialog_controller.js b/app/javascript/controllers/dialog_controller.js index 44876b231..e9f8f990c 100644 --- a/app/javascript/controllers/dialog_controller.js +++ b/app/javascript/controllers/dialog_controller.js @@ -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() + } + } } diff --git a/app/javascript/controllers/form_controller.js b/app/javascript/controllers/form_controller.js index 2b30c99b9..db1602feb 100644 --- a/app/javascript/controllers/form_controller.js +++ b/app/javascript/controllers/form_controller.js @@ -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) } diff --git a/app/javascript/controllers/pagination_controller.js b/app/javascript/controllers/pagination_controller.js index 01d93d909..cdc6258b5 100644 --- a/app/javascript/controllers/pagination_controller.js +++ b/app/javascript/controllers/pagination_controller.js @@ -90,7 +90,6 @@ export default class extends Controller { #replacePaginationLinkWithFrame(linkElement) { const turboFrame = this.#buildTurboFrameFor(linkElement) this.#insertTurboFrameAtPosition(linkElement, turboFrame) - linkElement.remove() } #buildTurboFrameFor(linkElement) { diff --git a/app/models/notification/bundle.rb b/app/models/notification/bundle.rb index 7c7ee6862..93110f87b 100644 --- a/app/models/notification/bundle.rb +++ b/app/models/notification/bundle.rb @@ -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 diff --git a/app/models/search.rb b/app/models/search.rb index 4fd6acf25..a51e757f6 100644 --- a/app/models/search.rb +++ b/app/models/search.rb @@ -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 diff --git a/app/models/user/day_timeline.rb b/app/models/user/day_timeline.rb index cf8d08612..d04a10683 100644 --- a/app/models/user/day_timeline.rb +++ b/app/models/user/day_timeline.rb @@ -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 diff --git a/app/models/webhook.rb b/app/models/webhook.rb index a766e68f5..d3411e974 100644 --- a/app/models/webhook.rb +++ b/app/models/webhook.rb @@ -15,7 +15,6 @@ class Webhook < ApplicationRecord card_due_date_removed card_published card_reopened - card_title_changed card_unassigned card_unstaged comment_created diff --git a/app/views/account/settings/_entropy_configuration.html.erb b/app/views/account/settings/_entropy_configuration.html.erb index 326ffa193..88dd539b2 100644 --- a/app/views/account/settings/_entropy_configuration.html.erb +++ b/app/views/account/settings/_entropy_configuration.html.erb @@ -1,5 +1,5 @@
-

Sink or Swim

-

Choose default settings for this account. You can override them in individual collections.

+

Auto close

+

Fizzy doesn’t let stale cards stick around forever. Cards automatically close as “Not now” without activity for specific period of time. This is the default, global setting — you can override it in each collection.

<%= render "entropy/auto_close", model: account.default_entropy_configuration, url: account_entropy_configuration_path %>
diff --git a/app/views/cards/display/_public_preview.html.erb b/app/views/cards/display/_public_preview.html.erb index 644cb49d4..5494815e0 100644 --- a/app/views/cards/display/_public_preview.html.erb +++ b/app/views/cards/display/_public_preview.html.erb @@ -3,7 +3,10 @@
- <%= card.id %> + + Card number + <%= card.id %> + <%= card.collection.name %> diff --git a/app/views/cards/display/common/_collection.html.erb b/app/views/cards/display/common/_collection.html.erb index 9a69481c9..ef1fe3a5e 100644 --- a/app/views/cards/display/common/_collection.html.erb +++ b/app/views/cards/display/common/_collection.html.erb @@ -1,5 +1,8 @@