diff --git a/Gemfile b/Gemfile index d5b333b2c..054167d6e 100644 --- a/Gemfile +++ b/Gemfile @@ -29,6 +29,7 @@ gem "rouge" gem "jbuilder" gem "actiontext-lexical", github: "basecamp/actiontext-lexical" gem "image_processing", "~> 1.14" +gem "platform_agent" # Telemetry and logging gem "sentry-ruby" diff --git a/Gemfile.lock b/Gemfile.lock index b3cefa8c6..5b9940aad 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,13 +1,6 @@ -GIT - remote: https://github.com/basecamp/actiontext-lexical.git - revision: 19038b86b8617062adbb930990edaacc1f643e3d - specs: - actiontext-lexical (0.1.0) - rails (>= 8.0.2) - GIT remote: https://github.com/basecamp/active_record-tenanted - revision: 173048c2af29e9a723811e552bc59e3154e82e2a + revision: 523947a0f0f49f66a8a62eb93e9ba1a345b3076b specs: active_record-tenanted (0.1.0) activerecord (>= 8.1.alpha) @@ -25,21 +18,20 @@ GIT GIT remote: https://github.com/crmne/ruby_llm.git - revision: 87a86f70cb64e23d5b76994edf8080618f07e071 + revision: 8412d51e869f1ee8e5229831a9a0bb5e9b8f3847 specs: - ruby_llm (1.3.0rc1) + ruby_llm (1.2.0) base64 event_stream_parser (~> 1) - faraday (>= 1.10.0) - faraday-multipart (>= 1) - faraday-net_http (>= 1) - faraday-retry (>= 1) - marcel (~> 1.0) + faraday (~> 2) + faraday-multipart (~> 1) + faraday-net_http (~> 3) + faraday-retry (~> 2) zeitwerk (~> 2) GIT remote: https://github.com/rails/rails.git - revision: bc7f77b32d7f3f26db654cae506c26a959852506 + revision: c1d7d56e831acf0aff5f3e27a6b384f543f358d0 branch: main specs: actioncable (8.1.0.alpha) @@ -73,6 +65,7 @@ GIT rails-html-sanitizer (~> 1.6) useragent (~> 0.16) actiontext (8.1.0.alpha) + action_text-trix (~> 2.1.15) actionpack (= 8.1.0.alpha) activerecord (= 8.1.0.alpha) activestorage (= 8.1.0.alpha) @@ -139,6 +132,8 @@ GIT GEM remote: https://rubygems.org/ specs: + action_text-trix (2.1.15) + railties addressable (2.8.7) public_suffix (>= 2.0.2, < 7.0) ast (2.4.3) @@ -147,7 +142,7 @@ GEM bcrypt_pbkdf (1.1.1) benchmark (0.4.0) bigdecimal (3.1.9) - bootsnap (1.18.4) + bootsnap (1.18.6) msgpack (~> 1.2) brakeman (7.0.2) racc @@ -193,10 +188,6 @@ GEM net-http (>= 0.5.0) faraday-retry (2.3.1) faraday (~> 2.0) - ffi (1.17.2) - ffi (1.17.2-arm64-darwin) - ffi (1.17.2-x86_64-darwin) - ffi (1.17.2-x86_64-linux-gnu) fugit (1.11.1) et-orbi (~> 1, >= 1.2.11) raabro (~> 1.4) @@ -205,12 +196,9 @@ GEM addressable (>= 2.5.0) globalid (1.2.1) activesupport (>= 6.1) - hashdiff (1.2.0) + hashdiff (1.1.2) i18n (1.14.7) concurrent-ruby (~> 1.0) - image_processing (1.14.0) - mini_magick (>= 4.9.5, < 6) - ruby-vips (>= 2.0.17, < 3) importmap-rails (2.1.0) actionpack (>= 6.0.0) activesupport (>= 6.0.0) @@ -224,7 +212,7 @@ GEM actionview (>= 5.0.0) activesupport (>= 5.0.0) json (2.11.3) - kamal (2.6.0) + kamal (2.6.1) activesupport (>= 7.0) base64 (~> 0.2) bcrypt_pbkdf (~> 1.0) @@ -248,9 +236,6 @@ GEM net-smtp marcel (1.0.4) matrix (0.4.2) - mini_magick (5.2.0) - benchmark - logger mini_mime (1.1.5) mini_portile2 (2.8.9) minitest (5.25.5) @@ -258,7 +243,7 @@ GEM multipart-post (2.4.1) net-http (0.6.0) uri - net-imap (0.5.7) + net-imap (0.5.8) date net-protocol net-pop (0.1.2) @@ -287,6 +272,9 @@ GEM parser (3.3.8.0) ast (~> 2.4.1) racc + platform_agent (1.0.1) + activesupport (>= 5.2.0) + useragent (~> 0.16.3) pp (0.6.2) prettyprint prettyprint (0.2.0) @@ -366,24 +354,21 @@ GEM rubocop-performance (>= 1.24) rubocop-rails (>= 2.30) ruby-progressbar (1.13.0) - ruby-vips (2.2.3) - ffi (~> 1.12) - logger rubyzip (2.4.1) securerandom (0.4.1) - selenium-webdriver (4.31.0) + selenium-webdriver (4.32.0) base64 (~> 0.2) logger (~> 1.4) rexml (~> 3.2, >= 3.2.5) rubyzip (>= 1.2.2, < 3.0) websocket (~> 1.0) - sentry-rails (5.23.0) + sentry-rails (5.24.0) railties (>= 5.0) - sentry-ruby (~> 5.23.0) - sentry-ruby (5.23.0) + sentry-ruby (~> 5.24.0) + sentry-ruby (5.24.0) bigdecimal concurrent-ruby (~> 1.0, >= 1.0.2) - solid_cable (3.0.7) + solid_cable (3.0.8) actioncable (>= 7.2) activejob (>= 7.2) activerecord (>= 7.2) @@ -463,7 +448,6 @@ PLATFORMS x86_64-mingw32 DEPENDENCIES - actiontext-lexical! active_record-tenanted! bcrypt (~> 3.1.7) bootsnap @@ -472,10 +456,10 @@ DEPENDENCIES capybara debug geared_pagination (~> 1.2) - image_processing (~> 1.14) importmap-rails jbuilder kamal + platform_agent propshaft puma (>= 5.0) rails! diff --git a/app/assets/stylesheets/comments.css b/app/assets/stylesheets/comments.css index f2babf69b..05d3518bc 100644 --- a/app/assets/stylesheets/comments.css +++ b/app/assets/stylesheets/comments.css @@ -37,8 +37,10 @@ } .comment__author { - margin-inline-end: calc(var(--comment-padding-inline) * -0.5); - + .comment--system & strong { + display: none; + } + .btn { font-weight: inherit; } @@ -51,9 +53,17 @@ .comment--mine_ & { margin-inline: calc(var(--comment-padding-inline) * -0.75); } + + .comment--system & { + display: none; + } } .comment__body { + .comment--system & { + text-align: center; + } + * { &:first-child { margin-block-start: 0; @@ -99,8 +109,13 @@ } } - .comment__event { + .comment--system { + --comment-padding-block: calc(var(--block-space) * 0.66); + + line-height: 1.2; max-inline-size: var(--comment-max); + text-align: center; + &::before { /* Make up space for lack of avatar */ @@ -108,5 +123,16 @@ display: flex; inline-size: calc(var(--comment-padding-inline) * 0.75); } + + .comment__author { + a { + margin: 0 auto; + } + } + + .comment__body { + margin-block-start: calc(var(--block-space-half) * -0.33); + text-align: center !important; + } } } diff --git a/app/assets/stylesheets/terminals.css b/app/assets/stylesheets/terminals.css index 0d23d000b..b709ad698 100644 --- a/app/assets/stylesheets/terminals.css +++ b/app/assets/stylesheets/terminals.css @@ -7,7 +7,7 @@ font-family: var(--font-mono); font-size: var(--text-normal); inset: auto 0 0 0; - padding: var(--block-space) calc(var(--tray-size) + var(--inline-space-double)); + padding: var(--block-space) calc(var(--tray-size) + calc(var(--inline-space) * 3)); position: fixed; z-index: var(--z-terminal); diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 4f75453ff..20385eda0 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -1,5 +1,5 @@ class ApplicationController < ActionController::Base - include Authentication, CurrentTimezone + include Authentication, CurrentTimezone, SetPlatform stale_when_importmap_changes allow_browser versions: :modern diff --git a/app/controllers/concerns/set_platform.rb b/app/controllers/concerns/set_platform.rb new file mode 100644 index 000000000..54e77d517 --- /dev/null +++ b/app/controllers/concerns/set_platform.rb @@ -0,0 +1,12 @@ +module SetPlatform + extend ActiveSupport::Concern + + included do + helper_method :platform + end + + private + def platform + @platform ||= ApplicationPlatform.new(request.user_agent) + end +end diff --git a/app/helpers/hotkeys_helper.rb b/app/helpers/hotkeys_helper.rb new file mode 100644 index 000000000..a1894104c --- /dev/null +++ b/app/helpers/hotkeys_helper.rb @@ -0,0 +1,19 @@ +module HotkeysHelper + def hotkey_label(hotkey) + hotkey.split(",").first if hotkey + case hotkey + when Array + hotkey.map { |key| + if key == "ctrl" && platform.mac? + "⌘" + elsif key == "enter" + platform.mac? ? "return" : "enter" + else + key + end + }.join("+") + else + hotkey.split(",").first if hotkey + end + end +end diff --git a/app/javascript/controllers/hotkey_controller.js b/app/javascript/controllers/hotkey_controller.js index 7f6e5f3bf..2106155f3 100644 --- a/app/javascript/controllers/hotkey_controller.js +++ b/app/javascript/controllers/hotkey_controller.js @@ -3,6 +3,7 @@ import { Controller } from "@hotwired/stimulus" export default class extends Controller { click(event) { if (this.#isClickable && !this.#shouldIgnore(event)) { + event.preventDefault() this.element.click() } } diff --git a/app/models/application_platform.rb b/app/models/application_platform.rb new file mode 100644 index 000000000..c8c24a50b --- /dev/null +++ b/app/models/application_platform.rb @@ -0,0 +1,29 @@ +class ApplicationPlatform < PlatformAgent + def ios? + match? /iPhone|iPad/ + end + + def android? + match? /Android/ + end + + def mac? + match? /Macintosh/ + end + + def chrome? + user_agent.browser.match? /Chrome/ + end + + def safari? + user_agent.browser.match? /Safari/ + end + + def mobile? + ios? || android? + end + + def desktop? + !mobile? + end +end diff --git a/app/models/card/eventable/system_commenter.rb b/app/models/card/eventable/system_commenter.rb index 2bd4e88fd..73247d2a3 100644 --- a/app/models/card/eventable/system_commenter.rb +++ b/app/models/card/eventable/system_commenter.rb @@ -8,46 +8,22 @@ class Card::Eventable::SystemCommenter def comment return unless comment_body.present? - if comment = find_replaceable_system_comment - comment.update! body: comment_body - else - card.comments.create! creator: User.system, body: comment_body, created_at: event.created_at - end + card.comments.create! creator: User.system, body: comment_body, created_at: event.created_at end private - RELATED_EVENTS = [ - %w[ card_assigned card_unassigned ] - ] - def comment_body case event.action when "card_assigned" - "Assigned to #{event.assignees.pluck(:name).to_sentence}." + "#{event.creator.name} assigned this to #{event.assignees.pluck(:name).to_sentence}." when "card_unassigned" - "Unassigned from #{event.assignees.pluck(:name).to_sentence}." + "#{event.creator.name} unassigned from #{event.assignees.pluck(:name).to_sentence}." when "card_staged" "#{event.creator.name} moved this to '#{event.stage_name}'." when "card_closed" - "Closed by #{ event.creator.name }" + "Closed as “#{ card.closure.reason }” by #{ event.creator.name }" when "card_title_changed" - "#{event.creator.name} changed title from '#{event.particulars.dig('particulars', 'old_title')}' to '#{event.particulars.dig('particulars', 'new_title')}'." + "#{event.creator.name} changed the title from '#{event.particulars.dig('particulars', 'old_title')}' to '#{event.particulars.dig('particulars', 'new_title')}'." end end - - def find_replaceable_system_comment - previous_comment, previous_event = card.comments.last, card.events[-2] - if previous_comment&.creator&.system? && replaceable_event?(previous_event, event) - previous_comment - end - end - - def replaceable_event?(candidate_event, replacement_event) - candidate_event && replacement_event && - (candidate_event.action == replacement_event.action || related_events?(candidate_event, replacement_event)) - end - - def related_events?(candidate_event, replacement_event) - RELATED_EVENTS.find { |related_actions| related_actions.include?(candidate_event.action) && related_actions.include?(replacement_event.action) } - end end diff --git a/app/models/command/go_to_card.rb b/app/models/command/go_to_card.rb index 0943413f3..a330d733b 100644 --- a/app/models/command/go_to_card.rb +++ b/app/models/command/go_to_card.rb @@ -4,7 +4,7 @@ class Command::GoToCard < Command validates_presence_of :card def title - "Visit card '#{card.title}'" + "Visit card '#{card&.title}'" end def execute @@ -13,6 +13,6 @@ class Command::GoToCard < Command private def card - user.accessible_cards.find(card_id) + user.accessible_cards.find_by_id(card_id) end end diff --git a/app/views/cards/comments/_comment.html.erb b/app/views/cards/comments/_comment.html.erb index feedc3667..a1e863bf9 100644 --- a/app/views/cards/comments/_comment.html.erb +++ b/app/views/cards/comments/_comment.html.erb @@ -1,6 +1,6 @@ <% cache comment do %> <%= turbo_frame_tag comment do %> -
<%= filter_title filter %> @@ -38,7 +38,7 @@ <%= render "cards/index/collections_filter", filter: filter %>
- Press ⌘j anytime to open this, esc to close. + Press <%= hotkey_label(["ctrl", "J"]) -%> anytime to open this, esc to close.
<% end %>- Press ⌘j anytime to open this, esc to close. + Press <%= hotkey_label(["ctrl", "K"]) -%> anytime to open this, esc to close.
<% end %>