Merge branch 'main' into seed-new-accounts

This commit is contained in:
Jason Zimdars
2025-11-03 17:10:09 -06:00
193 changed files with 1147 additions and 913 deletions
-3
View File
@@ -55,9 +55,6 @@ 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 "sqlite-vec", "0.1.7.alpha.2"
group :development, :test do
gem "debug"
gem "bundler-audit", require: false
+6 -11
View File
@@ -36,7 +36,7 @@ GIT
GIT
remote: https://github.com/rails/rails.git
revision: def13087d55a6dbbfa80d114c847555927ce2ed7
revision: d3d17c297a70d3e255a34693a1fb0074712e9930
branch: main
specs:
actioncable (8.2.0.alpha)
@@ -267,7 +267,7 @@ GEM
activesupport (>= 6.0.0)
railties (>= 6.0.0)
io-console (0.8.1)
irb (1.15.2)
irb (1.15.3)
pp (>= 0.6.0)
rdoc (>= 4.0.0)
reline (>= 0.4.2)
@@ -345,7 +345,7 @@ GEM
net-smtp (0.5.1)
net-protocol
net-ssh (7.3.0)
nio4r (2.7.4)
nio4r (2.7.5)
nokogiri (1.18.10)
mini_portile2 (~> 2.8.2)
racc (~> 1.4)
@@ -407,7 +407,7 @@ GEM
nio4r (~> 2.0)
raabro (1.4.0)
racc (1.8.1)
rack (3.2.3)
rack (3.2.4)
rack-session (2.1.1)
base64 (>= 0.1.0)
rack (>= 3.0.0)
@@ -427,7 +427,7 @@ GEM
rake-compiler-dock (1.9.1)
rb_sys (0.9.117)
rake-compiler-dock (= 1.9.1)
rdoc (6.15.0)
rdoc (6.15.1)
erb
psych (>= 4.0.0)
tsort
@@ -508,10 +508,6 @@ GEM
fugit (~> 1.11)
railties (>= 7.1)
thor (>= 1.3.1)
sqlite-vec (0.1.7.alpha.2-arm64-darwin)
sqlite-vec (0.1.7.alpha.2-arm64-linux)
sqlite-vec (0.1.7.alpha.2-x86_64-darwin)
sqlite-vec (0.1.7.alpha.2-x86_64-linux)
sqlite3 (2.7.4)
mini_portile2 (~> 2.8.0)
sqlite3 (2.7.4-arm64-darwin)
@@ -543,7 +539,7 @@ GEM
unicode-display_width (3.2.0)
unicode-emoji (~> 4.1)
unicode-emoji (4.1.0)
uri (1.0.4)
uri (1.1.0)
useragent (0.16.11)
vcr (6.3.1)
base64
@@ -646,7 +642,6 @@ DEPENDENCIES
solid_cable (>= 3.0)
solid_cache (~> 1.0)
solid_queue (~> 1.2)
sqlite-vec (= 0.1.7.alpha.2)
sqlite3 (>= 2.0)
stimulus-rails
thruster
+6
View File
@@ -198,6 +198,12 @@
z-index: var(--z-events-column-header);
}
.events__column-footer {
grid-row-start: 26;
margin: var(--events-gap);
padding: var(--block-space-half) var(--inline-space);
}
.events__time-block {
align-content: end;
display: grid;
@@ -0,0 +1,11 @@
class Account::EntropiesController < ApplicationController
def update
Account.sole.entropy.update!(entropy_params)
redirect_to account_settings_path, notice: "Account updated"
end
private
def entropy_params
params.expect(entropy: [ :auto_postpone_period ])
end
end
@@ -1,12 +0,0 @@
class Account::EntropyConfigurationsController < ApplicationController
def update
Entropy::Configuration.default.update!(entropy_configuration_params)
redirect_to account_settings_path, notice: "Account updated"
end
private
def entropy_configuration_params
params.expect(entropy_configuration: [ :auto_postpone_period ])
end
end
@@ -8,11 +8,8 @@ class Account::JoinCodesController < ApplicationController
end
def update
if @join_code.update(join_code_params)
redirect_to account_join_code_path
else
render :edit, status: :unprocessable_entity
end
@join_code.update!(join_code_params)
redirect_to account_join_code_path
end
def destroy
@@ -1,19 +1,21 @@
class Account::SettingsController < ApplicationController
before_action :ensure_admin, only: :update
before_action :set_account
def show
@account = Account.sole
@users = User.active.alphabetically
end
def update
if Current.user.can_administer?
Account.sole.update!(account_params)
redirect_to account_settings_path
else
head :forbidden
end
@account.update!(account_params)
redirect_to account_settings_path
end
private
def set_account
@account = Account.sole
end
def account_params
params.expect account: %i[ name ]
end
@@ -2,11 +2,11 @@ class Cards::AssignmentsController < ApplicationController
include CardScoped
def new
@users = @collection.users.active.alphabetically
fresh_when @users
end
def create
@card.toggle_assignment @collection.users.active.find(params[:assignee_id])
render turbo_stream: turbo_stream.replace([ @card, :assignees ], partial: "cards/display/perma/assignees", locals: { card: @card.reload })
end
end
@@ -2,15 +2,20 @@ class Cards::CollectionsController < ApplicationController
include CollectionScoped
skip_before_action :set_collection, only: %i[ edit ]
before_action :set_card
def edit
@card = Current.user.accessible_cards.find(params[:card_id])
@collections = Current.user.collections.ordered_by_recently_accessed
fresh_when @collections
end
def update
@card = Current.user.accessible_cards.find(params[:card_id])
@card.move_to(@collection)
redirect_to @card
end
private
def set_card
@card = Current.user.accessible_cards.find(params[:card_id])
end
end
+1
View File
@@ -2,6 +2,7 @@ class Cards::PinsController < ApplicationController
include CardScoped
def show
fresh_when etag: @card.pin_for(Current.user) || "none"
end
def create
@@ -1,7 +0,0 @@
class Cards::RecoversController < ApplicationController
include CardScoped
def create
redirect_to @card.recover_abandoned_creation
end
end
@@ -5,7 +5,6 @@ class Cards::StepsController < ApplicationController
def create
@step = @card.steps.create!(step_params)
render_card_replacement
end
def show
@@ -16,12 +15,10 @@ class Cards::StepsController < ApplicationController
def update
@step.update!(step_params)
render_card_replacement
end
def destroy
@step.destroy!
render_card_replacement
end
private
@@ -3,6 +3,7 @@ class Cards::TaggingsController < ApplicationController
def new
@tags = Tag.all.alphabetically
fresh_when @tags
end
def create
+2 -2
View File
@@ -5,11 +5,11 @@ class Cards::TriagesController < ApplicationController
column = @card.collection.columns.find(params[:column_id])
@card.triage_into(column)
render_card_replacement
redirect_to @card
end
def destroy
@card.send_back_to_triage
render_card_replacement
redirect_to @card
end
end
@@ -1,6 +1,10 @@
class Cards::WatchesController < ApplicationController
include CardScoped
def show
fresh_when etag: @card.watch_for(Current.user) || "none"
end
def create
@card.watch_by Current.user
end
+2 -2
View File
@@ -9,7 +9,7 @@ class CardsController < ApplicationController
end
def create
card = @collection.cards.create!
card = @collection.cards.find_or_create_by!(creator: Current.user, status: "drafted")
redirect_to card
end
@@ -30,7 +30,7 @@ class CardsController < ApplicationController
def destroy
@card.destroy!
redirect_to @card.collection, notice: ("Card deleted" unless @card.creating?)
redirect_to @card.collection, notice: "Card deleted"
end
private
@@ -1,7 +1,7 @@
class Collections::ColumnsController < ApplicationController
include ActionView::RecordIdentifier, CollectionScoped
include CollectionScoped
before_action :set_column, only: [ :show, :update, :destroy ]
before_action :set_column, only: %i[ show update destroy ]
def show
set_page_and_extract_portion_from @column.cards.active.latest.with_golden_first
@@ -26,6 +26,6 @@ class Collections::ColumnsController < ApplicationController
end
def column_params
params.require(:column).permit(:name, :color)
params.expect(column: [ :name, :color ])
end
end
@@ -0,0 +1,12 @@
class Collections::EntropiesController < ApplicationController
include CollectionScoped
def update
@collection.entropy.update!(entropy_params)
end
private
def entropy_params
params.expect(collection: [ :auto_postpone_period ])
end
end
@@ -1,17 +0,0 @@
class Collections::EntropyConfigurationsController < ApplicationController
include CollectionScoped
def update
@collection.entropy_configuration.update!(entropy_configuration_params)
render turbo_stream: [
turbo_stream.replace([ @collection, :entropy_configuration ], partial: "collections/edit/auto_close", locals: { collection: @collection }),
turbo_stream_flash(notice: "Saved")
]
end
private
def entropy_configuration_params
params.expect(collection: [ :auto_postpone_period ])
end
end
@@ -3,18 +3,10 @@ class Collections::PublicationsController < ApplicationController
def create
@collection.publish
render turbo_stream: [
turbo_stream.replace([ @collection, :publication ], partial: "collections/edit/publication", locals: { collection: @collection }),
turbo_stream_flash(notice: "Saved")
]
end
def destroy
@collection.unpublish
@collection.reload
render turbo_stream: [
turbo_stream.replace([ @collection, :publication ], partial: "collections/edit/publication", locals: { collection: @collection }),
turbo_stream_flash(notice: "Saved")
]
end
end
+8 -6
View File
@@ -1,11 +1,7 @@
class CollectionsController < ApplicationController
before_action :set_collection, except: %i[ new create ]
include FilterScoped
def new
@collection = Collection.new
end
before_action :set_collection, except: %i[ new create ]
def show
if @filter.used?(ignore_collections: true)
@@ -15,6 +11,10 @@ class CollectionsController < ApplicationController
end
end
def new
@collection = Collection.new
end
def create
@collection = Collection.create! collection_params.with_defaults(all_access: true)
redirect_to collection_path(@collection)
@@ -22,12 +22,14 @@ class CollectionsController < ApplicationController
def edit
selected_user_ids = @collection.users.pluck :id
@selected_users, @unselected_users = User.active.alphabetically.partition { |user| selected_user_ids.include? user.id }
@selected_users, @unselected_users = \
User.active.alphabetically.partition { |user| selected_user_ids.include? user.id }
end
def update
@collection.update! collection_params
@collection.accesses.revise granted: grantees, revoked: revokees if grantees_changed?
if @collection.accessible_to?(Current.user)
redirect_to edit_collection_path(@collection), notice: "Saved"
else
@@ -3,7 +3,5 @@ class Columns::Cards::Drops::ClosuresController < ApplicationController
def create
@card.close
render turbo_stream: turbo_stream.replace("closed-cards", partial: "collections/show/closed", method: :morph, locals: { collection: @card.collection })
end
end
@@ -1,10 +1,8 @@
class Columns::Cards::Drops::ColumnsController < ApplicationController
include ActionView::RecordIdentifier, CardScoped
include CardScoped
def create
column = @card.collection.columns.find(params[:column_id])
@card.triage_into(column)
render turbo_stream: turbo_stream.replace(dom_id(column), partial: "collections/show/column", method: :morph, locals: { column: column })
@column = @card.collection.columns.find(params[:column_id])
@card.triage_into(@column)
end
end
@@ -3,7 +3,5 @@ class Columns::Cards::Drops::NotNowsController < ApplicationController
def create
@card.postpone
render turbo_stream: turbo_stream.replace("not-now", partial: "collections/show/not_now", method: :morph, locals: { collection: @card.collection })
end
end
@@ -4,7 +4,5 @@ class Columns::Cards::Drops::StreamsController < ApplicationController
def create
@card.send_back_to_triage
set_page_and_extract_portion_from @collection.cards.awaiting_triage.latest.with_golden_first
render turbo_stream: turbo_stream.replace("the-stream", partial: "collections/show/stream", method: :morph, locals: { collection: @card.collection, page: @page })
end
end
+2 -1
View File
@@ -37,7 +37,7 @@ module Authentication
end
def require_tenant
unless ApplicationRecord.current_tenant.present?
if ApplicationRecord.current_tenant.blank?
redirect_to session_menu_url(script_name: nil)
end
end
@@ -52,6 +52,7 @@ module Authentication
end
end
# FIXME: Remove before launch
def clear_old_scoped_session_cookies
if request.script_name.present? && cookies.signed[:session_token].present? && !find_session_by_cookie
cookies.signed[:session_token] = { value: "invalid-session-token", path: request.script_name, expires: 1.hour.ago }
+1 -1
View File
@@ -29,7 +29,7 @@ module Authorization
if Current.membership.blank?
redirect_to session_menu_url(script_name: nil)
elsif Current.user.nil? && Current.membership.join_code.present?
redirect_to new_user_path
redirect_to new_users_join_path
elsif !Current.user&.active?
redirect_to unlink_membership_url(script_name: nil, membership_id: Current.membership.signed_id(purpose: :unlinking))
end
@@ -1,12 +0,0 @@
module CachedPublicly
extend ActiveSupport::Concern
included do
before_action :set_cache_headers
end
private
def set_cache_headers
expires_in 30.seconds, public: true
end
end
@@ -1,13 +0,0 @@
module PublicCardScoped
extend ActiveSupport::Concern
included do
before_action :set_collection_and_card
end
private
def set_collection_and_card
@collection = Collection.find_by_published_key(params[:collection_id])
@card = @collection.cards.find(params[:id])
end
end
@@ -1,12 +0,0 @@
module PublicCollectionScoped
extend ActiveSupport::Concern
included do
before_action :set_collection
end
private
def set_collection
@collection = Collection.find_by_published_key(params[:collection_id] || params[:id])
end
end
@@ -1,12 +0,0 @@
module Search::QueryTermsScoped
extend ActiveSupport::Concern
included do
before_action :set_query_terms
end
private
def set_query_terms
@query_terms = params[:q]
end
end
+6
View File
@@ -1,4 +1,10 @@
module TurboFlash
extend ActiveSupport::Concern
included do
helper_method :turbo_stream_flash
end
private
def turbo_stream_flash(**flash_options)
turbo_stream.replace(:flash, partial: "layouts/shared/flash", locals: { flash: flash_options })
+1
View File
@@ -1,5 +1,6 @@
class My::PinsController < ApplicationController
def index
@pins = Current.user.pins.includes(:card).ordered.limit(20)
fresh_when @pins
end
end
+21
View File
@@ -0,0 +1,21 @@
class Public::BaseController < ApplicationController
allow_unauthenticated_access
allow_unauthorized_access
before_action :set_collection, :set_card, :set_public_cache_expiration
layout "public"
private
def set_collection
@collection = Collection.find_by_published_key(params[:collection_id] || params[:id])
end
def set_card
@card = @collection.cards.find(params[:id]) if params[:collection_id] && params[:id]
end
def set_public_cache_expiration
expires_in 30.seconds, public: true
end
end
+1 -8
View File
@@ -1,11 +1,4 @@
class Public::CardsController < ApplicationController
include CachedPublicly, PublicCardScoped
allow_unauthenticated_access only: :show
allow_unauthorized_access only: :show
layout "public"
class Public::CardsController < Public::BaseController
def show
end
end
@@ -1,11 +1,4 @@
class Public::Collections::Columns::ClosedsController < ApplicationController
include CachedPublicly, PublicCollectionScoped
allow_unauthenticated_access only: :show
allow_unauthorized_access only: :show
layout "public"
class Public::Collections::Columns::ClosedsController < Public::BaseController
def show
set_page_and_extract_portion_from @collection.cards.closed.recently_closed_first
end
@@ -1,11 +1,4 @@
class Public::Collections::Columns::NotNowsController < ApplicationController
include CachedPublicly, PublicCollectionScoped
allow_unauthenticated_access only: :show
allow_unauthorized_access only: :show
layout "public"
class Public::Collections::Columns::NotNowsController < Public::BaseController
def show
set_page_and_extract_portion_from @collection.cards.postponed.reverse_chronologically.with_golden_first
end
@@ -1,11 +1,4 @@
class Public::Collections::Columns::StreamsController < ApplicationController
include CachedPublicly, PublicCollectionScoped
allow_unauthenticated_access only: :show
allow_unauthorized_access only: :show
layout "public"
class Public::Collections::Columns::StreamsController < Public::BaseController
def show
set_page_and_extract_portion_from @collection.cards.awaiting_triage.latest.with_golden_first
end
@@ -1,18 +1,15 @@
class Public::Collections::ColumnsController < ApplicationController
include ActionView::RecordIdentifier, CachedPublicly, PublicCollectionScoped
allow_unauthenticated_access only: :show
allow_unauthorized_access only: :show
layout "public"
before_action :set_column, only: :show
class Public::Collections::ColumnsController < Public::BaseController
before_action :set_column
def show
set_page_and_extract_portion_from @column.cards.active.latest.with_golden_first
end
private
# Unlike the other public controllers, this is using params[:id] to fetch the column instead of the card
def set_card
end
def set_column
@column = @collection.columns.find(params[:id])
end
@@ -1,11 +1,4 @@
class Public::CollectionsController < ApplicationController
include CachedPublicly, PublicCollectionScoped
allow_unauthenticated_access only: :show
allow_unauthorized_access only: :show
layout "public"
class Public::CollectionsController < Public::BaseController
def show
set_page_and_extract_portion_from @collection.cards.awaiting_triage.latest.with_golden_first
end
@@ -1,8 +1,6 @@
class Searches::QueriesController < ApplicationController
include Search::QueryTermsScoped
def create
Current.user.remember_search(@query_terms)
Current.user.remember_search(params[:q])
head :ok
end
end
+3 -3
View File
@@ -1,8 +1,8 @@
class SearchesController < ApplicationController
include Search::QueryTermsScoped, Turbo::DriveHelper
include Turbo::DriveHelper
def show
if card = Current.user.accessible_cards.find_by_id(@query_terms)
if card = Current.user.accessible_cards.find_by_id(params[:q])
@card = card
else
perform_search
@@ -11,7 +11,7 @@ class SearchesController < ApplicationController
private
def perform_search
set_page_and_extract_portion_from Current.user.search(@query_terms)
set_page_and_extract_portion_from Current.user.search(params[:q])
@recent_search_queries = Current.user.search_queries.order(updated_at: :desc).limit(10)
end
end
-1
View File
@@ -10,7 +10,6 @@ class SessionsController < ApplicationController
def create
Identity.find_by_email_address(email_address)&.send_magic_link
redirect_to session_magic_link_path
end
-29
View File
@@ -1,29 +0,0 @@
class UploadsController < ApplicationController
include ActiveStorage::SetCurrent
before_action :set_file, only: :create
before_action :set_attachment, only: :show
def create
# FIXME: Try to get upload attachments on root
@upload = Account.sole.uploads_attachments.create! blob: create_blob!
end
def show
expires_in 5.minutes, public: true
redirect_to @attachment.url, allow_other_host: true
end
private
def set_file
@file = params[:file]
end
def set_attachment
@attachment = ActiveStorage::Attachment.find_by! slug: "#{params[:slug]}.#{params[:format]}"
end
def create_blob!
ActiveStorage::Blob.create_and_upload! io: @file, filename: @file.original_filename, content_type: @file.content_type
end
end
+7 -10
View File
@@ -4,31 +4,28 @@ class Users::AvatarsController < ApplicationController
allow_unauthenticated_access only: :show
before_action :set_user
before_action :ensure_permission_to_administer_user, only: :destroy
def show
if stale? @user, cache_control: { max_age: cache_max_age, stale_while_revalidate: 1.week }
if stale? @user, cache_control: { max_age: (30.minutes unless Current.user == @user), stale_while_revalidate: 1.week }.compact
render_avatar_or_initials
end
end
def destroy
@user.avatar.destroy
redirect_to user_path(@user)
redirect_to @user
end
private
def cache_max_age
if Current.user == @user
0
else
30.minutes
end
end
def set_user
@user = User.find(params[:user_id])
end
def ensure_permission_to_administer_user
head :forbidden unless Current.user.admin? || Current.user == @user
end
def render_avatar_or_initials
if @user.avatar.attached?
send_blob_stream @user.avatar
@@ -0,0 +1,25 @@
class Users::EventsController < ApplicationController
include FilterScoped
before_action :set_user, :set_filter, :set_user_filtering
def show
@filter = Current.user.filters.new(creator_ids: [ @user.id ])
@day_timeline = Current.user.timeline_for(day_param, filter: @filter)
fresh_when @day_timeline
end
private
def set_user
@user = User.active.find(params[:user_id])
end
def day_param
if params[:day].present?
Time.zone.parse(params[:day])
else
Time.current
end
end
end
+31
View File
@@ -0,0 +1,31 @@
class Users::JoinsController < ApplicationController
require_access_without_a_user
before_action :set_join_code, :ensure_join_code_is_valid
def new
end
def create
@join_code.redeem do
User.create!(user_params.merge(membership: Current.membership))
end
redirect_to root_path
end
private
def set_join_code
@join_code = Account::JoinCode.active.find_by(code: Current.membership.join_code)
end
def ensure_join_code_is_valid
unless @join_code&.active?
redirect_to unlink_membership_url(script_name: nil, membership_id: Current.membership.signed_id(purpose: :unlinking))
end
end
def user_params
params.expect(user: [ :name, :avatar ])
end
end
+1 -1
View File
@@ -3,7 +3,7 @@ class Users::RolesController < ApplicationController
before_action :ensure_permission_to_administer_user
def update
@user.update(role_params)
@user.update!(role_params)
redirect_to users_path
end
-39
View File
@@ -1,32 +1,11 @@
class UsersController < ApplicationController
include FilterScoped
require_access_without_a_user only: %i[ new create ]
before_action :set_join_code, only: %i[ new create]
before_action :ensure_join_code_is_valid, only: %i[ new create ]
before_action :set_user, only: %i[ show edit update destroy ]
before_action :ensure_permission_to_change_user, only: %i[ update destroy ]
before_action :set_filter, only: %i[ edit show ]
before_action :set_user_filtering, only: %i[ edit show]
def new
end
def create
@join_code.redeem do
User.create!(user_params.merge(membership: Current.membership))
end
redirect_to root_path
end
def edit
end
def show
@filter = Current.user.filters.new(creator_ids: [ @user.id ])
@day_timeline = Current.user.timeline_for(day_param, filter: @filter)
end
def update
@@ -40,16 +19,6 @@ class UsersController < ApplicationController
end
private
def set_join_code
@join_code = Account::JoinCode.active.find_by(code: Current.membership.join_code)
end
def ensure_join_code_is_valid
unless @join_code&.active?
redirect_to unlink_membership_url(script_name: nil, membership_id: Current.membership.signed_id(purpose: :unlinking))
end
end
def set_user
@user = User.active.find(params[:id])
end
@@ -58,14 +27,6 @@ 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, :avatar ])
end
@@ -1,14 +1,10 @@
class Webhooks::ActivationsController < ApplicationController
before_action :ensure_admin
before_action :set_webhook
def create
@webhook.activate unless @webhook.active?
redirect_to @webhook, status: :see_other
end
webhook = Webhook.find(params[:webhook_id])
webhook.activate
private
def set_webhook
@webhook = Webhook.find(params[:webhook_id])
end
redirect_to webhook
end
end
+5 -8
View File
@@ -1,6 +1,7 @@
class WebhooksController < ApplicationController
include CollectionScoped
before_action :ensure_admin
before_action :set_collection
before_action :set_webhook, except: %i[ index new create ]
def index
@@ -15,8 +16,8 @@ class WebhooksController < ApplicationController
end
def create
@webhook = @collection.webhooks.create!(webhook_params)
redirect_to @webhook
webhook = @collection.webhooks.create!(webhook_params)
redirect_to webhook
end
def edit
@@ -29,14 +30,10 @@ class WebhooksController < ApplicationController
def destroy
@webhook.destroy!
redirect_to collection_webhooks_path, status: :see_other
redirect_to collection_webhooks_path
end
private
def set_collection
@collection = Collection.find(params[:collection_id])
end
def set_webhook
@webhook = @collection.webhooks.find(params[:id])
end
-20
View File
@@ -1,20 +0,0 @@
require "zlib"
module AiHelper
LLM_MODELS = %w[
gpt-5-chat-latest
gpt-5-mini
gpt-5-nano
gpt-5
chatgpt-4o-latest
gpt-4.1
gpt-3.5-turbo
gpt-4.1-mini
gpt-4.1-nano
gpt-4o-mini
]
def llm_model_options
LLM_MODELS.map { |model| [ model, model ] }
end
end
+1 -1
View File
@@ -1,6 +1,6 @@
module ApplicationHelper
def page_title_tag
tag.title @page_title || "BOXCAR"
tag.title @page_title || "Fizzy"
end
def icon_tag(name, **options)
+12 -6
View File
@@ -3,24 +3,30 @@ module EventsHelper
case event_type
when "added"
events = day_timeline.events.where(action: [ "card_published", "card_reopened" ])
full_events_count = events.count
{
title: event_column_title("Added", events.count, day_timeline.day),
title: event_column_title("Added", full_events_count, day_timeline.day),
index: 1,
events: events
events: events.limit(100).load,
full_events_count: full_events_count
}
when "closed"
events = day_timeline.events.where(action: "card_closed")
full_events_count = events.count
{
title: event_column_title("Closed", events.count, day_timeline.day),
title: event_column_title("Closed", full_events_count, day_timeline.day),
index: 3,
events: events
events: events.limit(100).load,
full_events_count: full_events_count
}
else
events = day_timeline.events.where.not(action: [ "card_published", "card_closed", "card_reopened" ])
full_events_count = events.count
{
title: event_column_title("Updated", events.count, day_timeline.day),
title: event_column_title("Updated", full_events_count, day_timeline.day),
index: 2,
events: events
events: events.limit(100).load,
full_events_count: full_events_count
}
end
end
+19
View File
@@ -0,0 +1,19 @@
module My::MenuHelper
def jump_field_tag
text_field_tag :search, nil,
type: "search",
role: "combobox",
placeholder: "Type to jump to a collection, person, place, or tag…",
class: "input input--transparent txt-small",
autofocus: true,
autocorrect: "off",
autocomplete: "off",
aria: { activedescendant: "" },
data: {
"1p-ignore": "true",
filter_target: "input",
nav_section_expander_target: "input",
navigable_list_target: "input",
action: "input->filter#filter" }
end
end
+14 -15
View File
@@ -2,7 +2,7 @@ import { Controller } from "@hotwired/stimulus"
import { post } from "@rails/request.js"
export default class extends Controller {
static targets = [ "turboFrame", "searchInput", "buttonsContainer" ]
static targets = [ "turboFrame", "search", "searchInput", "form", "buttonsContainer" ]
static outlets = [ "dialog" ]
static values = {
searchUrl: String,
@@ -17,18 +17,13 @@ export default class extends Controller {
this.dialogOutlet.close()
this.#clearTurboFrame()
this.searchInputTarget.querySelector("input").value = ""
this.#showItem(this.buttonsContainerTarget)
this.#hideItem(this.searchInputTarget)
this.#hideItem(this.searchTarget)
}
showModalAndSubmit(event) {
event.preventDefault()
const form = event.target.closest("form")
this.showModal()
form.requestSubmit()
this.formTarget.requestSubmit()
}
showModal() {
@@ -36,10 +31,18 @@ export default class extends Controller {
}
search(event) {
this.#openInTurboFrame(this.searchUrlValue)
this.#showItem(this.searchInputTarget)
this.#showItem(this.searchTarget)
this.#hideItem(this.buttonsContainerTarget)
if (this.searchInputTarget.value.trim()) {
this.showModalAndSubmit()
} else {
this.#loadTurboFrame()
}
}
#loadTurboFrame() {
this.turboFrameTarget.src = this.searchUrlValue
}
#clearTurboFrame() {
@@ -47,10 +50,6 @@ export default class extends Controller {
this.turboFrameTarget.innerHtml = ""
}
#openInTurboFrame(url) {
this.turboFrameTarget.src = url
}
#showItem(element) {
element.removeAttribute("hidden")
-7
View File
@@ -1,7 +0,0 @@
class Card::TouchAllJob < ApplicationJob
queue_as :backend
def perform(container)
container.cards.in_batches(&:touch_all)
end
end
@@ -1,7 +0,0 @@
class RemoveAbandonedCreationsJob < ApplicationJob
def perform
ApplicationRecord.with_each_tenant do |tenant|
Card.remove_abandoned_creations
end
end
end
+1 -1
View File
@@ -1,5 +1,5 @@
class ApplicationMailer < ActionMailer::Base
default from: "The BOXCAR team <support@37signals.com>"
default from: "The Fizzy team <support@37signals.com>"
layout "mailer"
append_view_path Rails.root.join("app/views/mailers")
+1 -1
View File
@@ -3,7 +3,7 @@ class MagicLinkMailer < ApplicationMailer
@magic_link = magic_link
@identity = @magic_link.identity
mail to: @identity.email_address, subject: "Sign in to BOXCAR"
mail to: @identity.email_address, subject: "Sign in to Fizzy"
end
private
+1 -1
View File
@@ -11,6 +11,6 @@ class Notification::BundleMailer < ApplicationMailer
mail \
to: bundle.user.identity.email_address,
subject: "Latest Activity in BOXCAR"
subject: "Latest Activity in Fizzy"
end
end
+1 -1
View File
@@ -16,7 +16,7 @@ class Account < ApplicationRecord
end
end
# To use the account as a generic card container. See +Entropy::Configuration+.
# To use the account as a generic card container. See +Entropy+.
def cards
Card.all
end
+4 -11
View File
@@ -1,17 +1,10 @@
module Account::Entropic
extend ActiveSupport::Concern
DEFAULT_ENTROPY_PERIOD = 30.days
included do
has_one :default_entropy_configuration, class_name: "Entropy::Configuration", as: :container, dependent: :destroy
before_save :set_default_entropy_configuration
has_one :entropy, as: :container, dependent: :destroy
after_create -> { create_entropy!(auto_postpone_period: DEFAULT_ENTROPY_PERIOD) }
end
private
DEFAULT_ENTROPY_PERIOD = 30.days
def set_default_entropy_configuration
self.default_entropy_configuration ||= build_default_entropy_configuration \
auto_postpone_period: DEFAULT_ENTROPY_PERIOD
end
end
+1 -5
View File
@@ -3,11 +3,7 @@ class Account::JoinCode < ApplicationRecord
scope :active, -> { where("usage_count < usage_limit") }
before_validation :generate_code, on: :create, if: -> { code.blank? }
validates :code, presence: true, uniqueness: true
validates :usage_limit, numericality: { only_integer: true, greater_than_or_equal_to: 0 }
validates :usage_count, numericality: { only_integer: true, greater_than_or_equal_to: 0 }
before_create :generate_code, if: -> { code.blank? }
def redeem
transaction do
+7 -3
View File
@@ -1,7 +1,7 @@
class Card < ApplicationRecord
include Assignable, Attachments, Closeable, Colored, Entropic, Eventable,
Golden, Mentions, Multistep, Pinnable, Postponable, Promptable, Readable,
Searchable, Stallable, Statuses, Taggable, Triageable, Watchable
include Assignable, Attachments, Broadcastable, Closeable, Colored, Entropic,
Eventable, Golden, Mentions, Multistep, Pinnable, Postponable, Promptable,
Readable, Searchable, Stallable, Statuses, Taggable, Triageable, Watchable
belongs_to :collection, touch: true
belongs_to :creator, class_name: "User", default: -> { Current.user }
@@ -52,6 +52,10 @@ class Card < ApplicationRecord
end
end
def filled?
title.present? || description.present?
end
private
def set_default_title
self.title = "Untitled" if title.blank?
+6 -4
View File
@@ -20,10 +20,12 @@ class Card::ActivitySpike::Detector
end
def register_activity_spike
if card.activity_spike
card.activity_spike.touch
else
card.create_activity_spike!
Card.suppressing_turbo_broadcasts do
if card.activity_spike
card.activity_spike.touch
else
card.create_activity_spike!
end
end
end
+7
View File
@@ -0,0 +1,7 @@
module Card::Broadcastable
extend ActiveSupport::Concern
included do
broadcasts_refreshes
end
end
+6 -6
View File
@@ -4,16 +4,16 @@ module Card::Entropic
included do
scope :due_to_be_postponed, -> do
active
.left_outer_joins(collection: :entropy_configuration)
.where("last_active_at <= DATETIME('now', '-' || COALESCE(entropy_configurations.auto_postpone_period, ?) || ' seconds')",
Entropy::Configuration.default.auto_postpone_period)
.left_outer_joins(collection: :entropy)
.where("last_active_at <= DATETIME('now', '-' || COALESCE(entropies.auto_postpone_period, ?) || ' seconds')",
Account.sole.entropy.auto_postpone_period)
end
scope :postponing_soon, -> do
active
.left_outer_joins(collection: :entropy_configuration)
.where("last_active_at > DATETIME('now', '-' || COALESCE(entropy_configurations.auto_postpone_period, ?) || ' seconds')", Entropy::Configuration.default.auto_postpone_period)
.where("last_active_at <= DATETIME('now', '-' || CAST(COALESCE(entropy_configurations.auto_postpone_period, ?) * 0.75 AS INTEGER) || ' seconds')", Entropy::Configuration.default.auto_postpone_period)
.left_outer_joins(collection: :entropy)
.where("last_active_at > DATETIME('now', '-' || COALESCE(entropies.auto_postpone_period, ?) || ' seconds')", Account.sole.entropy.auto_postpone_period)
.where("last_active_at <= DATETIME('now', '-' || CAST(COALESCE(entropies.auto_postpone_period, ?) * 0.75 AS INTEGER) || ' seconds')", Account.sole.entropy.auto_postpone_period)
end
delegate :auto_postpone_period, to: :collection
+5
View File
@@ -19,6 +19,7 @@ module Card::Eventable
def touch_last_active_at
# Not using touch so that we can detect attribute change on callbacks
update!(last_active_at: Time.current)
broadcast_activity
end
private
@@ -35,4 +36,8 @@ module Card::Eventable
def create_system_comment_for(event)
SystemCommenter.new(self, event).comment
end
def broadcast_activity
broadcast_render_later_to self, :activity, partial: "card/display/refresh_activity", locals: { card: self }
end
end
+4
View File
@@ -9,6 +9,10 @@ module Card::Pinnable
pins.exists?(user: user)
end
def pin_for(user)
pins.find_by(user: user)
end
def pin_by(user)
pins.find_or_create_by!(user: user)
end
+1 -22
View File
@@ -2,38 +2,17 @@ module Card::Statuses
extend ActiveSupport::Concern
included do
enum :status, %w[ creating drafted published ].index_by(&:itself)
enum :status, %w[ drafted published ].index_by(&:itself)
after_create -> { track_event :published }, if: :published?
scope :published_or_drafted_by, ->(user) { where(status: :published).or(where(status: :drafted, creator: user)) }
end
class_methods do
def remove_abandoned_creations
Card.creating.where(updated_at: ..1.day.ago).destroy_all
end
end
def can_recover_abandoned_creation?
abandoned_creations.where(updated_at: 1.day.ago..).any?
end
def recover_abandoned_creation
abandoned_creations.last.tap do |card|
Card.creating.where(creator: creator).excluding(card).destroy_all
end
end
def publish
transaction do
published!
track_event :published
end
end
private
def abandoned_creations
Card.creating.where(creator: creator).where("created_at != updated_at").excluding(self)
end
end
+5 -1
View File
@@ -9,7 +9,11 @@ module Card::Watchable
end
def watched_by?(user)
watchers.include?(user)
watch_for(user)&.watching?
end
def watch_for(user)
watches.find_by(user: user)
end
def watch_by(user)
-1
View File
@@ -8,7 +8,6 @@ class Collection < ApplicationRecord
has_many :tags, -> { distinct }, through: :cards
has_many :events
has_many :webhooks, dependent: :destroy
has_one :entropy_configuration, class_name: "Entropy::Configuration", as: :container, dependent: :destroy
scope :alphabetically, -> { order("lower(name)") }
scope :ordered_by_recently_accessed, -> { merge(Access.ordered_by_recently_accessed) }
+1 -6
View File
@@ -4,11 +4,6 @@ module Collection::Cards
included do
has_many :cards, dependent: :destroy
after_update_commit :touch_all_cards_later, if: :saved_change_to_name?
after_update_commit -> { cards.touch_all }, if: :saved_change_to_name?
end
private
def touch_all_cards_later
Card::TouchAllJob.perform_later(self)
end
end
+6 -5
View File
@@ -2,15 +2,16 @@ module Collection::Entropic
extend ActiveSupport::Concern
included do
delegate :auto_postpone_period, to: :entropy_configuration
delegate :auto_postpone_period, to: :entropy
has_one :entropy, as: :container, dependent: :destroy
end
def entropy_configuration
super || Entropy::Configuration.default
def entropy
super || Account.sole.entropy
end
def auto_postpone_period=(new_value)
entropy_configuration ||= association(:entropy_configuration).reader || self.build_entropy_configuration
entropy_configuration.update auto_postpone_period: new_value
entropy ||= association(:entropy).reader || self.build_entropy
entropy.update auto_postpone_period: new_value
end
end
+3 -23
View File
@@ -4,27 +4,7 @@ class Column < ApplicationRecord
belongs_to :collection, touch: true
has_many :cards, dependent: :nullify
validates :name, presence: true
validates :color, presence: true
before_validation :set_default_color
after_save_commit :touch_all_cards_later, if: :should_invalidate_cards?
after_destroy_commit :touch_all_collection_cards
private
def set_default_color
self.color ||= Card::DEFAULT_COLOR
end
def touch_all_cards_later
Card::TouchAllJob.perform_later(self)
end
def should_invalidate_cards?
saved_change_to_name? || saved_change_to_color?
end
def touch_all_collection_cards
Card::TouchAllJob.perform_later(collection)
end
before_validation -> { self.color ||= Card::DEFAULT_COLOR }
after_save_commit -> { cards.touch_all }, if: -> { saved_change_to_name? || saved_change_to_color? }
after_destroy_commit -> { collection.cards.touch_all }
end
+4 -4
View File
@@ -1,5 +1,5 @@
module Entropy
def self.table_name_prefix
"entropy_"
end
class Entropy < ApplicationRecord
belongs_to :container, polymorphic: true
after_commit -> { container.cards.touch_all }
end
-16
View File
@@ -1,16 +0,0 @@
class Entropy::Configuration < ApplicationRecord
belongs_to :container, polymorphic: true
after_commit :touch_all_cards_later
class << self
def default
Account.sole.default_entropy_configuration
end
end
private
def touch_all_cards_later
Card::TouchAllJob.perform_later(container)
end
end
+2
View File
@@ -3,6 +3,8 @@ class Step < ApplicationRecord
scope :completed, -> { where(completed: true) }
validates :content, presence: true
def completed?
completed
end
-2
View File
@@ -1,6 +1,4 @@
class User::Filtering
include Rails.application.routes.url_helpers
attr_reader :user, :filter, :expanded
delegate :as_params, :single_collection, to: :filter
+2 -2
View File
@@ -40,11 +40,11 @@ class Webhook < ApplicationRecord
validate :validate_url
def activate
update_columns active: true
update! active: true unless active?
end
def deactivate
update_columns active: false
update! active: false
end
def renderer
+3 -6
View File
@@ -1,7 +1,6 @@
<% @page_title = "Change usage limit" %>
<% content_for :header do %>
<%= render "my/menus/menu" %>
<div class="header__actions header__actions--start">
<%= link_to account_join_code_path, class: "btn borderless txt-medium", data: { controller: "hotkey", action: "keydown.esc@document->hotkey#click" } do %>
<span class="overflow-ellipsis">&larr; <strong>Invite link</strong></span>
@@ -14,13 +13,11 @@
<h2 class="txt-large margin-none font-weight-black"><%= @page_title %></h2>
<p class="txt-medium margin-none">How many times can this link be used to join the account?</p>
</header>
<%= form_with model: @join_code, url: account_join_code_path, method: :patch, data: { controller: "form" }, html: { class: "flex flex-column gap" } do |form| %>
<%= form.number_field :usage_limit,
required: true,
autofocus: true,
min: @join_code.usage_count,
class: "input center txt-large fit-content font-weight-black txt-align-center",
style: "max-inline-size: 8ch",
required: true, autofocus: true, min: @join_code.usage_count,
class: "input center txt-large fit-content font-weight-black txt-align-center", style: "max-inline-size: 8ch",
data: { action: "keydown.esc@document->form#cancel focus->form#select" } %>
<p class="margin-none txt-subtle">
+5 -6
View File
@@ -1,7 +1,6 @@
<% @page_title = "Add people" %>
<% content_for :header do %>
<%= render "my/menus/menu" %>
<div class="header__actions header__actions--start">
<%= link_to account_settings_path, class: "btn borderless txt-medium", data: { controller: "hotkey", action: "keydown.esc@document->hotkey#click" } do %>
<span class="overflow-ellipsis">&larr; <strong>Account Settings</strong></span>
@@ -18,8 +17,8 @@
<% url = join_url(code: @join_code.code, tenant: @join_code.tenant, script_name: nil) %>
<div class="flex align-center gap-half">
<input type="text" class="input flex-item-grow" value="<%= url %>" readonly>
<%= button_to account_join_code_path, method: :delete, class: "btn btn--circle txt-small",
data: { turbo_confirm: "Are you sure you want to generate a new link? The previous code will stop working." } do %>
<%= button_to account_join_code_path, method: :delete, class: "btn btn--circle txt-small", data: {
turbo_confirm: "Are you sure you want to generate a new link? The previous code will stop working." } do %>
<%= icon_tag "refresh" %>
<span class="for-screen-reader">Generate a new code</span>
<% end %>
@@ -27,9 +26,8 @@
<div class="center flex align-center gap">
<%= tag.button class: "btn btn--link", data: {
controller: "copy-to-clipboard", action: "copy-to-clipboard#copy",
copy_to_clipboard_success_class: "btn--success", copy_to_clipboard_content_value: url } do %>
controller: "copy-to-clipboard", action: "copy-to-clipboard#copy",
copy_to_clipboard_success_class: "btn--success", copy_to_clipboard_content_value: url } do %>
<%= icon_tag "copy-paste" %>
<span class="txt-nowrap">Copy invite link</span>
<% end %>
@@ -60,6 +58,7 @@
<p class="margin-none <%= "txt-negative" if !@join_code.active? %>">
This code has been used <%= @join_code.usage_count %>/<%= @join_code.usage_limit %> times
(<%= @join_code.active? ? @join_code.usage_limit - @join_code.usage_count : "none" %> remaining)
<%= link_to edit_account_join_code_path, class: @join_code.active? ? "txt-link" : "txt-negative txt-underline" do %>
<span>Change limit</span>
<% end %>
@@ -0,0 +1,8 @@
<div class="settings__panel settings__panel--entropy panel shadow center">
<header>
<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 on each board.</em></p>
</header>
<%= render "entropy/auto_close", model: account.entropy, url: account_entropy_path %>
</div>
@@ -1,8 +0,0 @@
<div class="settings__panel settings__panel--entropy panel shadow center">
<header>
<h2 class="divider txt-large">Auto close</h2>
<p class="margin-none-block-start">BOXCAR 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 on each board.</em></p>
</header>
<%= render "entropy/auto_close", model: account.default_entropy_configuration, url: account_entropy_configuration_path %>
</div>
+1 -1
View File
@@ -10,5 +10,5 @@
<%= render "account/settings/users", users: @users %>
</div>
<%= render "account/settings/entropy_configuration", account: @account %>
<%= render "account/settings/entropy", account: @account %>
</section>
+1 -1
View File
@@ -7,7 +7,7 @@
<% end %>
</div>
<div class="bar__input" data-bar-target="searchInput" hidden>
<div class="bar__input" data-bar-target="search" hidden>
<%= render "searches/form", query_terms: "" %>
</div>
@@ -0,0 +1,4 @@
<%= turbo_stream.remove dom_id(card, "bubble") %>
<%= turbo_stream.replace dom_id(card, :card_closure_toggle) do %>
<%= render "cards/container/closure", card: card %>
<% end %>
+1 -1
View File
@@ -38,7 +38,7 @@
<% if card.published? %>
<%= render "cards/container/footer/published", card: card %>
<% elsif card.creating? %>
<% elsif card.drafted? %>
<%= render "cards/container/footer/draft", card: card %>
<% end %>
</section>
@@ -0,0 +1 @@
<%= turbo_stream.replace([ @card, :meta ], partial: "/cards/display/perma/meta", method: "morph", locals: { card: @card.reload }) %>
+3 -4
View File
@@ -7,17 +7,16 @@
navigable_list_actionable_items_value: true } do %>
<strong class="popup__title">Assign this to…</strong>
<% if @collection.users.active.count > 1 %>
<% if @users.many? %>
<%= text_field_tag :search, nil, placeholder: "Filter…", class: "input input--transparent txt-small margin-block-half", autofocus: true,
type: "search", autocorrect: "off", autocomplete: "off", data: { "1p-ignore": "true", filter_target: "input", action: "input->filter#filter" } %>
<% end %>
<ul class="popup__list" data-filter-target="list">
<% @collection.users.active.alphabetically.each do |user| %>
<% @users.each do |user| %>
<li class="popup__item" data-filter-target="item" data-navigable-list-target="item" aria-checked="<%= @card.assignees.include?(user) %>">
<%= button_to card_assignments_path(@card, params: { assignee_id: user.id }), method: :post,
class: "popup__btn btn",
form_class: "max-width flex-item-grow" do %>
class: "popup__btn btn", form_class: "max-width flex-item-grow" do %>
<span class="overflow-ellipsis flex-item-grow"><%= user.name %></span>
<%= icon_tag "check", size: 18, class: "checked flex-item-no-shrink flex-item-justify-end", style: "--icon-size: 1em" %>
<% end %>
+8 -5
View File
@@ -10,10 +10,13 @@
<%= button_to card_closure_path(card), class: "btn borderless" do %>
<span class="overflow-ellipsis">Mark as Done</span>
<% end %>
<% if card.entropic? && card.open? %>
<div class="card-perma__closure-message">
Closes as “Not Now” <%= local_datetime_tag(card.entropy.auto_clean_at, style: :indays) -%> if theres no activity.
</div>
<% end %>
<div id="<%= dom_id(card, :closure_notice) %>">
<% if card.entropic? && card.open? && !card.postponed? %>
<div class="card-perma__closure-message">
Closes as “Not Now” <%= local_datetime_tag(card.entropy.auto_clean_at, style: :indays) -%> if theres no activity.
</div>
<% end %>
</div>
<% end %>
</div>
+1 -10
View File
@@ -1,13 +1,4 @@
<% if card.creating? && card.can_recover_abandoned_creation? %>
<div class="card__banner min-width max-width" style="--card-color: <%= card.color %>;">
<span class="overflow-ellipsis">You have an unsaved card. Would you like to continue where you left off?</span>
<%= button_to card_recover_path(card), class: "btn txt-small", data: { turbo_action: "replace" } do %>
<span>Restore</span>
<% end %>
</div>
<% end %>
<% if card.drafted? %>
<% if card.drafted? && card.filled? %>
<div class="card__banner min-width max-width" style="--card-color: <%= card.color %>;">
<span class="overflow-ellipsis">This is a draft, its only visible to you.</span>
<%= button_to card_publish_path(@card), class: "btn txt-small", style: "--btn-background: #{card.color}" do %>
@@ -1,8 +1,7 @@
<div class="card-perma__notch card-perma__notch--bottom">
<%= button_to "Create card", card_publish_path(card), name: "creation_type", value: "add", class: "btn" %>
<%= button_to "Create card", card_publish_path(card), name: "creation_type", value: "add", class: "btn",
form: { data: { controller: "form", action: "keydown.ctrl+enter@document->form#submit:prevent keydown.meta+enter@document->form#submit:prevent" } } %>
<%= button_to "Create and add another", card_publish_path(card), method: :post, class: "btn btn--reversed", name: "creation_type", value: "add_another",
title: "Create and add another (ctrl+shift+enter)", form: { data: { turbo: false } }, data: { controller: "hotkey", action: "keydown.ctrl+shift+enter@document->hotkey#click" } %>
<%= button_to "Save as a draft", card_path(card), name: "card[status]", value: "drafted", method: :put, class: "btn btn--reversed" %>
</div>
@@ -9,7 +9,7 @@
<span class="for-screen-reader">Assign</span>
</button>
<dialog class="popup panel flex-column align-start gap-half fill-white shadow" data-dialog-target="dialog">
<dialog class="popup panel flex-column align-start gap-half fill-white shadow" data-dialog-target="dialog" data-action="turbo:before-morph-attribute->dialog#preventCloseOnMorphing turbo:submit-end->dialog#close">
<%= yield %>
</dialog>
</div>
@@ -15,13 +15,9 @@
</span>
<span class="card__meta-text card__meta-text--updated overflow-ellipsis">
<% if card.creating? %>
&nbsp;
<% else %>
<%= icon_tag "refresh--meta" %>
Updated
<%= local_datetime_tag(card.last_active_at, style: :daysago) %>
<% end %>
<%= icon_tag "refresh--meta" %>
Updated
<%= local_datetime_tag(card.last_active_at, style: :daysago) %>
</span>
<span class="card__meta-text card__meta-text--assignees overflow-ellipsis">
@@ -1,3 +1,3 @@
<%= render "cards/display/common/assignees", card: card do %>
<%= turbo_frame_tag card, :assignment, src: new_card_assignment_path(card) %>
<%= turbo_frame_tag card, :assignment, src: new_card_assignment_path(card), refresh: "morph" %>
<% end %>
@@ -1,14 +1,12 @@
<%= render "cards/display/common/collection", card: card do %>
<% if Current.user.collections.count > 1 %>
<div class="align-self-center position-relative txt-small" data-controller="dialog" data-action="keydown.esc->dialog#close:stop click@document->dialog#closeOnClickOutside" <%= "hidden" if card.closed? %>>
<button class="collection-picker__button btn btn--reversed card__hide-on-index fill-transparent" data-action="click->dialog#open:stop">
<%= icon_tag "caret-down" %>
<span class="for-screen-reader">Choose a collection for this card</span>
</button>
<div class="align-self-center position-relative txt-small" data-controller="dialog" data-action="keydown.esc->dialog#close:stop click@document->dialog#closeOnClickOutside" <%= "hidden" if card.closed? %>>
<button class="collection-picker__button btn btn--reversed card__hide-on-index fill-transparent" data-action="click->dialog#open:stop">
<%= icon_tag "caret-down" %>
<span class="for-screen-reader">Choose a collection for this card</span>
</button>
<dialog class="popup panel flex-column align-start gap-half fill-white shadow margin-block-double" data-dialog-target="dialog">
<%= turbo_frame_tag "collection_picker", src: edit_card_collection_path(card), target: "_top" %>
</dialog>
</div>
<% end %>
<dialog class="popup panel flex-column align-start gap-half fill-white shadow margin-block-double" data-dialog-target="dialog" data-action="turbo:before-morph-attribute->dialog#preventCloseOnMorphing turbo:submit-end->dialog#close">
<%= turbo_frame_tag "collection_picker", src: edit_card_collection_path(card), target: "_top", refresh: "morph" %>
</dialog>
</div>
<% end %>
@@ -1,9 +1,9 @@
<ol class="steps txt-small margin-block-start-auto">
<%= render partial: "cards/steps/step", collection: card.steps, as: :step %>
<li class="step">
<li id="<%= dom_id(card, :new_step) %>" class="step">
<input type="checkbox" class="step__checkbox" disabled>
<%= form_with model: [card, Step.new], url: card_steps_path(card), data: { controller: "form", action: "submit->form#preventEmptySubmit" } do |form| %>
<%= form_with model: [card, Step.new], url: card_steps_path(card), data: { controller: "form", action: "submit->form#preventEmptySubmit turbo:submit-end->form#reset" } 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>
+3 -3
View File
@@ -5,12 +5,12 @@
<span class="for-screen-reader">Add a tag</span>
</button>
<dialog class="popup panel flex-column align-start justify-start fill-white shadow txt-small" data-dialog-target="dialog">
<%= turbo_frame_tag card, :tagging, src: new_card_tagging_path(card) %>
<dialog class="popup panel flex-column align-start justify-start fill-white shadow txt-small" data-dialog-target="dialog" data-action="turbo:before-morph-attribute->dialog#preventCloseOnMorphing turbo:submit-end->dialog#close">
<%= turbo_frame_tag card, :tagging, src: new_card_tagging_path(card), refresh: "morph" %>
</dialog>
</div>
<% if card.tags.any? || card.creating? %>
<% if card.tags.any? %>
<div class="min-width overflow-ellipsis">
<% card.tags.each_with_index do |tag, index| %>
<%= link_to cards_path(collection_ids: [ card.collection ], tag_ids: [ tag.id ]),

Some files were not shown because too many files have changed in this diff Show More