diff --git a/.kamal/hooks/pre-connect b/.kamal/hooks/pre-connect index 11cbb0a33..2b6d9af42 100755 --- a/.kamal/hooks/pre-connect +++ b/.kamal/hooks/pre-connect @@ -1,7 +1,32 @@ #!/usr/bin/env bash -# -# Verify Tailscale connection and SSH authentication before deploying. +# Validate hostnames are FQDNs ending in -int.37signals.com +if command -v yq >/dev/null 2>&1; then + declare -A SUGGESTIONS + while IFS= read -r host; do + if [[ ! $host =~ -int\.37signals\.com$ ]]; then + if [[ $host =~ -4[0-9]{2}$ ]]; then + SUGGESTIONS["$host"]="$host.df-ams-int.37signals.com" + elif [[ $host =~ -1[0-9]{2}$ ]]; then + SUGGESTIONS["$host"]="$host.df-iad-int.37signals.com" + else + SUGGESTIONS["$host"]="$host.sc-chi-int.37signals.com" + fi + fi + done < <(bin/kamal config -d "${KAMAL_DESTINATION:-production}" 2>/dev/null | yq '.":hosts"[]') + + if [ ${#SUGGESTIONS[@]} -gt 0 ]; then + echo "Unqualified hostnames found in config/deploy.${KAMAL_DESTINATION:-production}.yml:" >&2 + echo "" >&2 + echo "Update to use fully-qualified hostnames:" >&2 + for host in "${!SUGGESTIONS[@]}"; do + echo " $host → ${SUGGESTIONS[$host]}" >&2 + done + exit 1 + fi +fi + +# Verify Tailscale connection and SSH authentication before deploying. tailscale_cmd() { if command -v tailscale >/dev/null 2>&1; then tailscale "$@" diff --git a/Dockerfile b/Dockerfile index 26bd97953..3e21f7f38 100644 --- a/Dockerfile +++ b/Dockerfile @@ -75,7 +75,9 @@ ENV RUBY_GC_HEAP_0_INIT_SLOTS=692636 \ RUBY_GC_HEAP_1_INIT_SLOTS=175943 \ RUBY_GC_HEAP_2_INIT_SLOTS=148807 \ RUBY_GC_HEAP_3_INIT_SLOTS=9169 \ - RUBY_GC_HEAP_4_INIT_SLOTS=3054 + RUBY_GC_HEAP_4_INIT_SLOTS=3054 \ + RUBY_GC_MALLOC_LIMIT=33554432 \ + RUBY_GC_MALLOC_LIMIT_MAX=67108864 # Start the server by default, this can be overwritten at runtime EXPOSE 80 443 9394 diff --git a/Dockerfile.dev b/Dockerfile.dev new file mode 100644 index 000000000..8ec772ce1 --- /dev/null +++ b/Dockerfile.dev @@ -0,0 +1,46 @@ +# syntax = docker/dockerfile:1 + +# Make sure RUBY_VERSION matches the Ruby version in .ruby-version +ARG RUBY_VERSION=3.4.7 +FROM registry.docker.com/library/ruby:$RUBY_VERSION-slim AS base + +# Rails app lives here +WORKDIR /rails + +# Set development environment +ENV RAILS_ENV="development" \ + BUNDLE_PATH="/usr/local/bundle" + +# Install packages needed for development +RUN apt-get update -qq && \ + apt-get install --no-install-recommends -y curl libsqlite3-0 libvips build-essential pkg-config git ffmpeg groff libreoffice-writer libreoffice-impress libreoffice-calc mupdf-tools libyaml-dev libssl-dev && \ + rm -rf /var/lib/apt/lists /var/cache/apt/archives + +# Install application gems +COPY Gemfile Gemfile.lock .ruby-version ./ +COPY lib/bootstrap.rb ./lib/bootstrap.rb +COPY gems ./gems/ +RUN --mount=type=secret,id=GITHUB_TOKEN --mount=type=cache,id=fizzy-devbundle-${RUBY_VERSION},sharing=locked,target=/devbundle \ + gem install bundler && \ + BUNDLE_PATH=/devbundle BUNDLE_GITHUB__COM="$(cat /run/secrets/GITHUB_TOKEN):x-oauth-basic" bundle install && \ + cp -a /devbundle/. "$BUNDLE_PATH"/ && \ + bundle clean --force + +# Fetch beamer library +FROM registry.37signals.com/basecamp/beamer:vfs AS beamer + +# Final stage +FROM base + +# Copy beamer +COPY --from=beamer /home/beamer/bin/beamer.so /usr/local/lib/beamer/beamer.so +COPY --from=beamer /usr/local/bin/beamer /usr/local/bin/beamer + +# Entrypoint prepares the database +ENTRYPOINT ["/rails/bin/docker-entrypoint"] + +# Start the server by default +EXPOSE 3000 +EXPOSE 3006 +EXPOSE 5001 +CMD ["./bin/thrust", "./bin/rails", "server"] diff --git a/Gemfile b/Gemfile index 566fc6326..578fa9316 100644 --- a/Gemfile +++ b/Gemfile @@ -1,7 +1,8 @@ source "https://rubygems.org" git_source(:bc) { |repo| "https://github.com/basecamp/#{repo}" } -gem "rails", github: "rails/rails", branch: "main" +# gem "rails", github: "rails/rails", branch: "main" +gem "rails", github: "flavorjones/rails", branch: "flavorjones/improve-sqlite-url-support" # Assets & front end gem "importmap-rails" @@ -10,7 +11,7 @@ gem "stimulus-rails" gem "turbo-rails" # Deployment and drivers -gem "activerecord-tenanted" +gem "activerecord-tenanted", bc: "activerecord-tenanted" gem "bootsnap", require: false gem "kamal", require: false gem "puma", ">= 5.0" @@ -40,6 +41,7 @@ gem "sentry-ruby" gem "sentry-rails" gem "rails_structured_logging", bc: "rails-structured-logging" gem "yabeda" +gem "yabeda-actioncable" gem "yabeda-activejob", github: "rosa/yabeda-activejob", branch: "support-end-time-in-seconds" gem "yabeda-gc" gem "yabeda-http_requests" @@ -49,6 +51,7 @@ gem "yabeda-rails" gem "webrick" # required for yabeda-prometheus metrics server gem "prometheus-client-mmap", "~> 1.1" gem "autotuner" +gem "benchmark" # indirect dependency, being removed from Ruby 3.5 stdlib so here to quash warnings # AI gem "ruby_llm", git: "https://github.com/crmne/ruby_llm.git" diff --git a/Gemfile.lock b/Gemfile.lock index 6acd98a6e..2463a90b5 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,3 +1,12 @@ +GIT + remote: https://github.com/basecamp/activerecord-tenanted + revision: f412b25ff0096064a4763dc0c8d21b6593678137 + specs: + activerecord-tenanted (0.5.0) + activerecord (>= 8.1.beta) + railties (>= 8.1.beta) + zeitwerk + GIT remote: https://github.com/basecamp/queenbee-plugin revision: eb01c697de1ad028afc65cc7d9b5345a7a8e849f @@ -31,9 +40,9 @@ GIT zeitwerk (~> 2) GIT - remote: https://github.com/rails/rails.git - revision: d8795bd762ab2716b6e7fd07a403566b28159cdc - branch: main + remote: https://github.com/flavorjones/rails.git + revision: aa964b6012c614909d8524a497e3e79b956b3ade + branch: flavorjones/improve-sqlite-url-support specs: actioncable (8.1.0.beta1) actionpack (= 8.1.0.beta1) @@ -96,12 +105,12 @@ GIT marcel (~> 1.0) activesupport (8.1.0.beta1) base64 - benchmark (>= 0.3) bigdecimal concurrent-ruby (~> 1.0, >= 1.3.1) connection_pool (>= 2.2.5) drb i18n (>= 1.6, < 2) + json logger (>= 1.4.2) minitest (>= 5.1) securerandom (>= 0.3) @@ -157,10 +166,6 @@ GEM activemodel (>= 5.0.0.a) activesupport (>= 5.0.0.a) builder (~> 3.1) - activerecord-tenanted (0.4.1) - activerecord (>= 8.1.beta) - railties (>= 8.1.beta) - zeitwerk activeresource (6.2.0) activemodel (>= 7.0) activemodel-serializers-xml (~> 1.0) @@ -172,7 +177,7 @@ GEM ast (2.4.3) autotuner (1.0.2) aws-eventstream (1.4.0) - aws-partitions (1.1170.0) + aws-partitions (1.1172.0) aws-sdk-core (3.233.0) aws-eventstream (~> 1, >= 1.3.0) aws-partitions (~> 1, >= 1.992.0) @@ -194,7 +199,7 @@ GEM bcrypt (3.1.20) bcrypt_pbkdf (1.1.1) benchmark (0.4.1) - bigdecimal (3.3.0) + bigdecimal (3.3.1) bootsnap (1.18.6) msgpack (~> 1.2) brakeman (7.1.0) @@ -229,7 +234,7 @@ GEM drb (2.2.3) dry-initializer (3.2.0) ed25519 (1.4.0) - erb (5.0.3) + erb (5.1.1) erubi (1.13.1) et-orbi (1.4.0) tzinfo @@ -276,7 +281,7 @@ GEM actionview (>= 7.0.0) activesupport (>= 7.0.0) jmespath (1.6.2) - json (2.15.1) + json (2.13.2) jwt (3.1.2) base64 kamal (2.7.0) @@ -410,7 +415,7 @@ GEM nio4r (~> 2.0) raabro (1.4.0) racc (1.8.1) - rack (3.2.2) + rack (3.2.3) rack-session (2.1.1) base64 (>= 0.1.0) rack (>= 3.0.0) @@ -480,9 +485,11 @@ GEM ruby2_keywords (0.0.5) rubyzip (3.1.1) securerandom (0.4.1) - selenium-webdriver (4.35.0) + selenium-webdriver (4.36.0) base64 (~> 0.2) + json (<= 2.13.2) logger (~> 1.4) + prism (~> 1.0, < 1.5) rexml (~> 3.2, >= 3.2.5) rubyzip (>= 1.2.2, < 4.0) websocket (~> 1.0) @@ -500,7 +507,7 @@ GEM activejob (>= 7.2) activerecord (>= 7.2) railties (>= 7.2) - solid_cache (1.0.7) + solid_cache (1.0.8) activejob (>= 7.2) activerecord (>= 7.2) railties (>= 7.2) @@ -576,6 +583,11 @@ GEM anyway_config (>= 1.0, < 3) concurrent-ruby dry-initializer + yabeda-actioncable (0.2.2) + actioncable (>= 7.2) + activesupport (>= 7.2) + railties (>= 7.2) + yabeda (~> 0.8) yabeda-gc (0.4.0) yabeda (~> 0.6) yabeda-http_requests (0.2.1) @@ -605,11 +617,12 @@ PLATFORMS x86_64-mingw32 DEPENDENCIES - activerecord-tenanted + activerecord-tenanted! activeresource autotuner aws-sdk-s3 bcrypt (~> 3.1.7) + benchmark bootsnap brakeman bundler-audit @@ -655,6 +668,7 @@ DEPENDENCIES webmock webrick yabeda + yabeda-actioncable yabeda-activejob! yabeda-gc yabeda-http_requests diff --git a/Procfile.beamer-testbed b/Procfile.beamer-testbed new file mode 100644 index 000000000..e24b485ff --- /dev/null +++ b/Procfile.beamer-testbed @@ -0,0 +1,3 @@ +web: bin/thrust bin/rails server +beamer: beamer --debug --directory ./storage/ run --primary ${BEAMER_PRIMARY:-$(hostname)} +migrations: ./bin/beamer-testbed run-migrations --debug --directory ./storage/ diff --git a/app/assets/stylesheets/card-columns.css b/app/assets/stylesheets/card-columns.css index 20674f264..324c02997 100644 --- a/app/assets/stylesheets/card-columns.css +++ b/app/assets/stylesheets/card-columns.css @@ -142,6 +142,18 @@ .card { inline-size: calc((100% - var(--cards-gap) * (var(--card-grid-columns) - 1) ) / var(--card-grid-columns)); } + + .card__header .card__column-name--current { + --btn-padding: 0.1em 0.5em; + + background: none; + border: 1px solid currentColor; + color: var(--card-color); + display: inline-flex; + flex: 0 1 auto; + inline-size: fit-content; + margin: 0 0 0 auto; + } } /* Column Elements @@ -261,7 +273,7 @@ writing-mode: vertical-rl; } - .cards:not(.is-collapsed) & { + .cards:not(.is-collapsed, .cards--considering) & { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; @@ -399,7 +411,7 @@ .card:has(.card__background img:not([src=""])) { .card__content, .card__meta, - .workflow-stage:not(.workflow-stage--current) { + .card__column-name:not(.card__column-name--current) { opacity: 0; transition: opacity 0.2s ease-in-out; @@ -651,6 +663,7 @@ /* -------------------------------------------------------------------------- */ /* Surface a mini bubble if there are cards with bubbles inside */ + .cards--considering:has(.bubble:not([hidden])) .cards__expander-title, .cards--doing.is-collapsed:has(.bubble:not([hidden])) { --bubble-color: var(--card-color, oklch(var(--lch-blue-medium))); --bubble-shape: 54% 46% 61% 39% / 57% 49% 51% 43%; @@ -670,4 +683,53 @@ z-index: 1; } } + + .cards--considering:has(.bubble:not([hidden])) .cards__expander-title:before { + translate: 120% 50%; + } + + /* Card column indicators + /* -------------------------------------------------------------------------- */ + + .card__column-name { + --btn-background: transparent; + --btn-padding: 0.2em 0.5em; + --btn-border-size: 0; + --btn-border-radius: 0.2em; + + color: inherit; + inline-size: 100%; + justify-content: flex-start; + text-transform: uppercase; + + @media (hover: hover) { + &:hover { + --btn-background: color(from var(--card-color) srgb r g b / 0.15); + } + } + } + + .card__column-name--current { + --btn-background: var(--card-color); + + color: var(--color-ink-inverted); + + @media (hover: hover) { + &:hover { + --btn-background: var(--card-color); + } + } + } + + @supports(mask: radial-gradient(black)) { + .card__column-name--stream:before { + background: white; + content: ""; + inset: 0; + mask: var(--wavy-background-mask); + position: absolute; + z-index: 0; + opacity: 0.25; + } + } } diff --git a/app/assets/stylesheets/card-perma.css b/app/assets/stylesheets/card-perma.css index 20cc97984..d56ce29b3 100644 --- a/app/assets/stylesheets/card-perma.css +++ b/app/assets/stylesheets/card-perma.css @@ -52,7 +52,7 @@ } .card__title { - font-size: clamp(var(--text-medium), 6vw, var(--text-xx-large)); + font-size: clamp(var(--text-medium), 6vw, var(--text-x-large)); } .card__meta, @@ -74,7 +74,7 @@ } } - .workflow-stage { + .card__column-name { @media (max-width: 639px) { --btn-border-radius: 0; } diff --git a/app/assets/stylesheets/cards.css b/app/assets/stylesheets/cards.css index e9eb53de6..add8f892f 100644 --- a/app/assets/stylesheets/cards.css +++ b/app/assets/stylesheets/cards.css @@ -50,7 +50,7 @@ max-inline-size: unset; min-inline-size: 0; - .workflow-stage { + .card__column-name { display: none; } } diff --git a/app/assets/stylesheets/color-picker.css b/app/assets/stylesheets/color-picker.css index 7519bfe50..af4601181 100644 --- a/app/assets/stylesheets/color-picker.css +++ b/app/assets/stylesheets/color-picker.css @@ -1,40 +1,16 @@ @layer components { - .color-picker { - --color-picker-origin: calc(var(--btn-size) / 2 + var(--color-picker-spacer)); - --color-picker-spacer: 0.5ch; - - background-color: var(--color-canvas); - border-radius: calc(var(--btn-size) / 2 + var(--color-picker-spacer)); - gap: var(--color-picker-spacer); - inline-size: auto !important; - inset: calc(-1 * var(--color-picker-spacer)) auto auto calc(-1 * var(--color-picker-spacer)); - max-inline-size: var(--panel-size) !important; - padding: var(--color-picker-spacer); - position: absolute; - transform: scale(0.5); - transform-origin: var(--color-picker-origin) var(--color-picker-origin); - transition: 150ms allow-discrete; - transition-property: display, opacity, overlay, transform; - z-index: var(--z-popup);opacity: 0; - - &[open] { - display: flex; - opacity: 1; - transform: scale(1); - } - - @starting-style { - &[open] { - opacity: 0; - transform: scale(0.5); - } - } - } - .color-picker__colors { display: grid; - grid-template-columns: repeat(4, 1fr); - gap: var(--color-picker-spacer); + grid-template-columns: repeat(3, 1fr); + gap: var(--inline-space-half); + + .btn { + --btn-border-radius: 0.1em; + --btn-size: 2em; + --icon-size: 1.3em; + + inline-size: 100%; + } } } diff --git a/app/assets/stylesheets/filters.css b/app/assets/stylesheets/filters.css index 5999d2598..7e609bf29 100644 --- a/app/assets/stylesheets/filters.css +++ b/app/assets/stylesheets/filters.css @@ -106,13 +106,19 @@ .filter__terms:is(.input) { --input-background: var(--color-canvas); --input-border-radius: 5em; - --input-padding: 0.5em 2.3em 0.5em 1.3em; + --input-padding: 0.5em 1.3em; + --input-width: 24ch; + --collapsed-filter-space: calc(var(--btn-size) + var(--inline-space-half) + 0.25em); - inline-size: 25ch; - min-inline-size: 25ch; + inline-size: var(--input-width); + min-inline-size: var(--input-width); .filters:not(.filters--expanded, .filters--has-filters-set) & { + --input-padding: 0.5em 2.7em 0.5em 1.3em; + + inline-size: calc(var(--input-width) + (0.25 * var(--collapsed-filter-space))); margin-inline-end: calc((var(--btn-size) + var(--inline-space-half) + 0.25em) * -1); + min-inline-size: calc(var(--input-width) + (0.25 * var(--collapsed-filter-space))); } } diff --git a/app/assets/stylesheets/trays.css b/app/assets/stylesheets/trays.css index 11c902cf5..194057807 100644 --- a/app/assets/stylesheets/trays.css +++ b/app/assets/stylesheets/trays.css @@ -423,7 +423,7 @@ margin-block-start: 0.3em; } - .workflow-stage--current { + .card__column-name--current { --btn-padding: 0.1em 0.5em; background: none !important; diff --git a/app/assets/stylesheets/workflows.css b/app/assets/stylesheets/workflows.css deleted file mode 100644 index 5baee3422..000000000 --- a/app/assets/stylesheets/workflows.css +++ /dev/null @@ -1,101 +0,0 @@ -@layer components { - .workflow-stage { - --btn-background: transparent; - --btn-padding: 0.2em 0.5em; - --btn-border-size: 0; - --btn-border-radius: 0.2em; - - color: inherit; - inline-size: 100%; - justify-content: flex-start; - text-transform: uppercase; - - @media (hover: hover) { - &:hover { - --btn-background: color(from var(--card-color) srgb r g b / 0.15); - } - } - } - - .workflow-stage--current { - --btn-background: var(--card-color); - - color: var(--color-ink-inverted); - - @media (hover: hover) { - &:hover { - --btn-background: var(--card-color); - } - } - } - - @supports(mask: radial-gradient(black)) { - .workflow-stage--stream:before { - background: white; - content: ""; - inset: 0; - mask: var(--wavy-background-mask); - position: absolute; - z-index: 0; - opacity: 0.25; - } - } - - /* Workflow settings - /* ------------------------------------------------------------------------ */ - - .workflow-preview { - --border-size: 0.2em; - - aspect-ratio: 1; - border: 0.2em solid var(--color-ink-lighter); - flex: 1 1 30%; - max-inline-size: 30%; - - @media (any-hover: hover) { - &:hover:not(.workflow-preview--selected) { - border-color: var(--color-ink-light); - } - } - - button { - appearance: none; - aspect-ratio: 1; - background: transparent; - block-size: 100%; - border: none; - cursor: pointer; - display: flex; - flex-direction: column; - gap: 0.5em; - inline-size: 100%; - margin: 0; - max-inline-size: 100%; - outline: 0; - overflow: hidden; - padding: var(--inline-space); - text-align: start; - } - } - - .workflow-preview--selected { - border-color: var(--color-link); - } - - .workflow-preview__swatch { - --btn-size: 1em; - --btn-border-radius: 50%; - --btn-border-size: 0; - - cursor: auto; - } - - .workflow__swatch-btn { - - @media (any-hover: hover) { - &:hover { - filter: brightness(0.9); - } - } - } -} diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index aa35df0b3..1de45b5c9 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -1,5 +1,5 @@ class ApplicationController < ActionController::Base - include Authentication, Authorization, CurrentRequest, CurrentTimezone, SetPlatform, TurboFlash, WriterAffinity + include Authentication, Authorization, CurrentRequest, CurrentTimezone, SetPlatform, TurboFlash, ViewTransitions, WriterAffinity stale_when_importmap_changes allow_browser versions: :modern, block: -> { render "errors/not_acceptable", layout: "error" } diff --git a/app/controllers/collections/columns/streams_controller.rb b/app/controllers/collections/columns/streams_controller.rb index 16a2bf671..6ff567c9b 100644 --- a/app/controllers/collections/columns/streams_controller.rb +++ b/app/controllers/collections/columns/streams_controller.rb @@ -2,7 +2,7 @@ class Collections::Columns::StreamsController < ApplicationController include CollectionScoped def show - set_page_and_extract_portion_from @collection.cards.awaiting_triage.reverse_chronologically.with_golden_first + set_page_and_extract_portion_from @collection.cards.awaiting_triage.by_last_activity.with_golden_first fresh_when etag: [ @collection, @page.records ] end end diff --git a/app/controllers/collections/columns_controller.rb b/app/controllers/collections/columns_controller.rb index 01f1704bf..30c5d38fd 100644 --- a/app/controllers/collections/columns_controller.rb +++ b/app/controllers/collections/columns_controller.rb @@ -4,7 +4,7 @@ class Collections::ColumnsController < ApplicationController before_action :set_column, only: [ :show, :update, :destroy ] def show - set_page_and_extract_portion_from @column.cards.active.reverse_chronologically.with_golden_first + set_page_and_extract_portion_from @column.cards.active.by_last_activity.with_golden_first fresh_when etag: [ @column, @page.records ] end diff --git a/app/controllers/collections_controller.rb b/app/controllers/collections_controller.rb index d14f26c32..005bf595c 100644 --- a/app/controllers/collections_controller.rb +++ b/app/controllers/collections_controller.rb @@ -51,7 +51,7 @@ class CollectionsController < ApplicationController end def show_columns - set_page_and_extract_portion_from @collection.cards.awaiting_triage.reverse_chronologically.with_golden_first + set_page_and_extract_portion_from @collection.cards.awaiting_triage.by_last_activity.with_golden_first fresh_when etag: [ @collection, @page.records, @user_filtering ] end diff --git a/app/controllers/concerns/view_transitions.rb b/app/controllers/concerns/view_transitions.rb new file mode 100644 index 000000000..94b9c0a55 --- /dev/null +++ b/app/controllers/concerns/view_transitions.rb @@ -0,0 +1,16 @@ +module ViewTransitions + extend ActiveSupport::Concern + + included do + before_action :disable_view_transitions, if: :page_refresh? + end + + private + def disable_view_transitions + @disable_view_transition = true + end + + def page_refresh? + request.referrer.present? && request.referrer == request.url + end +end diff --git a/app/controllers/public/collections/columns/streams_controller.rb b/app/controllers/public/collections/columns/streams_controller.rb index 18a90aa83..904cab2e5 100644 --- a/app/controllers/public/collections/columns/streams_controller.rb +++ b/app/controllers/public/collections/columns/streams_controller.rb @@ -6,7 +6,7 @@ class Public::Collections::Columns::StreamsController < ApplicationController layout "public" def show - set_page_and_extract_portion_from @collection.cards.awaiting_triage.reverse_chronologically.with_golden_first + set_page_and_extract_portion_from @collection.cards.awaiting_triage.by_last_activity.with_golden_first # To enable caching at intermediate proxies during traffic spikes expires_in 5.seconds, public: true diff --git a/app/controllers/public/collections/columns_controller.rb b/app/controllers/public/collections/columns_controller.rb index c2ed5fba3..bd9341234 100644 --- a/app/controllers/public/collections/columns_controller.rb +++ b/app/controllers/public/collections/columns_controller.rb @@ -8,7 +8,7 @@ class Public::Collections::ColumnsController < ApplicationController before_action :set_column, only: :show def show - set_page_and_extract_portion_from @column.cards.active.reverse_chronologically.with_golden_first + set_page_and_extract_portion_from @column.cards.active.by_last_activity.with_golden_first # To enable caching at intermediate proxies during traffic spikes expires_in 5.seconds, public: true diff --git a/app/controllers/public/collections_controller.rb b/app/controllers/public/collections_controller.rb index 0aacb1f3f..1e208f766 100644 --- a/app/controllers/public/collections_controller.rb +++ b/app/controllers/public/collections_controller.rb @@ -6,7 +6,7 @@ class Public::CollectionsController < ApplicationController layout "public" def show - set_page_and_extract_portion_from @collection.cards.awaiting_triage.reverse_chronologically.with_golden_first + set_page_and_extract_portion_from @collection.cards.awaiting_triage.by_last_activity.with_golden_first # To enable caching at intermediate proxies during traffic spikes expires_in 5.seconds, public: true diff --git a/app/helpers/cards_helper.rb b/app/helpers/cards_helper.rb index 07a6b931f..2ab894086 100644 --- a/app/helpers/cards_helper.rb +++ b/app/helpers/cards_helper.rb @@ -33,8 +33,4 @@ module CardsHelper title << "assigned to #{card.assignees.map(&:name).to_sentence}" if card.assignees.any? title.join(" ") end - - def cacheable_preview_parts_for(card, *options) - [ card, card.collection, card.collection.entropy_configuration, card.collection.publication, card.column&.color, *options ] - end end diff --git a/app/helpers/collections_helper.rb b/app/helpers/collections_helper.rb new file mode 100644 index 000000000..ac1debab4 --- /dev/null +++ b/app/helpers/collections_helper.rb @@ -0,0 +1,5 @@ +module CollectionsHelper + def referenced_collections_from(records) + Current.user.collections.where id: records.pluck(:collection_id).uniq + end +end diff --git a/app/helpers/columns_helper.rb b/app/helpers/columns_helper.rb index 008cc0404..402124455 100644 --- a/app/helpers/columns_helper.rb +++ b/app/helpers/columns_helper.rb @@ -4,7 +4,7 @@ module ColumnsHelper tag.span(column.name, class: "overflow-ellipsis"), card_triage_path(card, column_id: column), method: :post, - class: [ "workflow-stage btn", { "workflow-stage--current": column == card.column } ], + class: [ "card__column-name btn", { "card__column-name--current": column == card.column } ], form_class: "flex align-center gap-half", data: { turbo_frame: "_top" } end diff --git a/app/helpers/entropy_helper.rb b/app/helpers/entropy_helper.rb index ee69d30be..f1a50bbe9 100644 --- a/app/helpers/entropy_helper.rb +++ b/app/helpers/entropy_helper.rb @@ -7,7 +7,7 @@ module EntropyHelper { daysBeforeReminder: card.entropy.days_before_reminder, closesAt: card.entropy.auto_clean_at.iso8601, - action: "Postpones" + action: "Closes" } end diff --git a/app/javascript/controllers/hotkey_controller.js b/app/javascript/controllers/hotkey_controller.js index a6b8192c9..813c69ef6 100644 --- a/app/javascript/controllers/hotkey_controller.js +++ b/app/javascript/controllers/hotkey_controller.js @@ -8,6 +8,13 @@ export default class extends Controller { } } + focus(event) { + if (this.#isClickable && !this.#shouldIgnore(event)) { + event.preventDefault() + this.element.focus() + } + } + #shouldIgnore(event) { return event.defaultPrevented || event.target.closest("input, textarea, lexxy-editor") } diff --git a/app/jobs/sqlite_backups_job.rb b/app/jobs/sqlite_backups_job.rb index 10b0efe37..66b7894ca 100644 --- a/app/jobs/sqlite_backups_job.rb +++ b/app/jobs/sqlite_backups_job.rb @@ -92,7 +92,7 @@ class SQLiteBackupsJob < ApplicationJob end def db_path(tenant) - db_config.database_path + db_config.config_adapter.database_path end def db_config diff --git a/app/models/card.rb b/app/models/card.rb index 2368d10ed..83a96dd6a 100644 --- a/app/models/card.rb +++ b/app/models/card.rb @@ -17,6 +17,7 @@ class Card < ApplicationRecord scope :reverse_chronologically, -> { order created_at: :desc, id: :desc } scope :chronologically, -> { order created_at: :asc, id: :asc } scope :latest, -> { order updated_at: :desc, id: :desc } + scope :by_last_activity, -> { order last_active_at: :desc, id: :desc } scope :indexed_by, ->(index) do case index diff --git a/app/models/filter.rb b/app/models/filter.rb index 12b51418a..e3ff45510 100644 --- a/app/models/filter.rb +++ b/app/models/filter.rb @@ -65,6 +65,6 @@ class Filter < ApplicationRecord private def include_closed_cards? - only_closed? || card_ids.present? + only_closed? || card_ids.present? || creator_ids.present? end end diff --git a/app/views/cards/_broadcasts.html.erb b/app/views/cards/_broadcasts.html.erb new file mode 100644 index 000000000..d2404ae68 --- /dev/null +++ b/app/views/cards/_broadcasts.html.erb @@ -0,0 +1,3 @@ +<% referenced_collections_from(records).each do |collection| %> + <%= turbo_stream_from collection %> +<% end %> diff --git a/app/views/cards/container/_closure.html.erb b/app/views/cards/container/_closure.html.erb index 4e250b958..772b86f5e 100644 --- a/app/views/cards/container/_closure.html.erb +++ b/app/views/cards/container/_closure.html.erb @@ -22,7 +22,7 @@ <% if card.entropic? && card.open? %>
- Moves to "Not now" if no activity <%= local_datetime_tag(card.entropy.auto_clean_at, style: :indays) -%>. + Closes as “Not Now” <%= local_datetime_tag(card.entropy.auto_clean_at, style: :indays) -%> if there’s no activity.
<% end %> <% end %> diff --git a/app/views/cards/display/_preview.html.erb b/app/views/cards/display/_preview.html.erb index ce8f3d026..07a070a9a 100644 --- a/app/views/cards/display/_preview.html.erb +++ b/app/views/cards/display/_preview.html.erb @@ -9,7 +9,7 @@ <%= icon_tag "attachment", class: "card__attachments-indicator translucent" if card.has_attachments? %> <% if card.triaged? %> - + <%= card.column.name %> <% end %> diff --git a/app/views/cards/display/preview/_columns.html.erb b/app/views/cards/display/preview/_columns.html.erb index b99a37930..e6ab890ab 100644 --- a/app/views/cards/display/preview/_columns.html.erb +++ b/app/views/cards/display/preview/_columns.html.erb @@ -1,5 +1,5 @@
<% card.collection.columns.each do |column| %> - <%= tag.span column.name, class: ["workflow-stage btn overflow-ellipsis", { "workflow-stage--current": column == card.column }] %> + <%= tag.span column.name, class: ["card__column-name btn overflow-ellipsis", { "card__column-name--current": column == card.column }] %> <% end %>
diff --git a/app/views/cards/display/public_preview/_columns.html.erb b/app/views/cards/display/public_preview/_columns.html.erb index 459893b7f..699c4c7a3 100644 --- a/app/views/cards/display/public_preview/_columns.html.erb +++ b/app/views/cards/display/public_preview/_columns.html.erb @@ -1,6 +1,6 @@
<% card.collection.columns.each do |column| %> <%= tag.div column.name, - class: ["workflow-stage overflow-ellipsis flex align-center gap-half btn non-clickable no-hover", { "workflow-stage--current": column == card.column }] %> + class: ["card__column-name overflow-ellipsis flex align-center gap-half btn non-clickable no-hover", { "card__column-name--current": column == card.column }] %> <% end %>
diff --git a/app/views/cards/index.html.erb b/app/views/cards/index.html.erb index 99121d028..d700b363a 100644 --- a/app/views/cards/index.html.erb +++ b/app/views/cards/index.html.erb @@ -1,7 +1,7 @@ <% @page_title = @user_filtering.selected_collections_label %> <% turbo_exempts_page_from_cache %> -<%= render "filters/broadcasts", filter: @filter %> +<%= render "cards/broadcasts", records: @page.records %> <% content_for :head do %> <%= tag.meta property: "og:title", content: @page_title %> @@ -28,7 +28,7 @@ <%= turbo_frame_tag :cards_container do %>
<%= 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) } %> + <%= render partial: "cards/display/preview", collection: @page.records, as: :card, locals: { draggable: true }, cached: ->(card) { card.cache_invalidation_parts.for_preview } %> <% end %>
<% end %> diff --git a/app/views/cards/triage/_columns.html.erb b/app/views/cards/triage/_columns.html.erb index ee0e1e24e..c77f06529 100644 --- a/app/views/cards/triage/_columns.html.erb +++ b/app/views/cards/triage/_columns.html.erb @@ -2,13 +2,13 @@ Choose a column for this card <%= button_to "Not now", card_not_now_path(card), - class: [ "workflow-stage btn", { "workflow-stage--current": card.postponed? } ], + class: [ "card__column-name btn", { "card__column-name--current": card.postponed? } ], role: "radio", aria: { checked: card.postponed? }, form_class: "flex align-center gap-half" %> <%= button_to "The Stream", card_triage_path(card), method: :delete, - class: [ "workflow-stage workflow-stage--stream btn", { "workflow-stage--current": card.awaiting_triage? } ], + class: [ "card__column-name card__column-name--stream btn", { "card__column-name--current": card.awaiting_triage? } ], role: "radio", aria: { checked: card.awaiting_triage? }, form_class: "flex align-center gap-half" %> @@ -18,7 +18,7 @@ <% end %> <%= button_to "Done", card_closure_path(card, reason: "Completed"), - class: [ "workflow-stage btn", { "workflow-stage--current": card.closed? } ], + class: [ "card__column-name btn", { "card__column-name--current": card.closed? } ], role: "radio", aria: { checked: card.closed? }, form_class: "flex align-center gap-half" %> diff --git a/app/views/collections/show/_filtered_cards.html.erb b/app/views/collections/show/_filtered_cards.html.erb index 63afd2920..9925e1e0a 100644 --- a/app/views/collections/show/_filtered_cards.html.erb +++ b/app/views/collections/show/_filtered_cards.html.erb @@ -1,5 +1,5 @@
<%= with_automatic_pagination :filtered_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) } %> + <%= render partial: "cards/display/preview", collection: page.records, as: :card, locals: { draggable: true }, cached: ->(card) { card.cache_invalidation_parts.for_preview } %> <% end %>
diff --git a/app/views/collections/show/_stream.html.erb b/app/views/collections/show/_stream.html.erb index 706bc8901..4f60bc29c 100644 --- a/app/views/collections/show/_stream.html.erb +++ b/app/views/collections/show/_stream.html.erb @@ -5,7 +5,7 @@
-

The Stream

+

The Stream

<%= link_to collection_columns_stream_path(collection), class: "btn btn--circle txt-x-small borderless cards__expander-button", data: { turbo_frame: "_top" } do %> <%= icon_tag "expand", class: "translucent" %> Expand column diff --git a/app/views/collections/show/menu/_column_form.html.erb b/app/views/collections/show/menu/_column_form.html.erb index 0b4cb854c..1382d4745 100644 --- a/app/views/collections/show/menu/_column_form.html.erb +++ b/app/views/collections/show/menu/_column_form.html.erb @@ -1,10 +1,10 @@ <%= 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: "Column name", value: column.name, - required: true, autocomplete: "off", pattern: ".*\\S.*", title: "Column name cannot be blank" %> -
- <% Card::COLORS.each do |color| %> -