Merge branch 'main' into seed-new-accounts
This commit is contained in:
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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
|
||||
@@ -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,5 +1,6 @@
|
||||
class My::PinsController < ApplicationController
|
||||
def index
|
||||
@pins = Current.user.pins.includes(:card).ordered.limit(20)
|
||||
fresh_when @pins
|
||||
end
|
||||
end
|
||||
|
||||
@@ -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,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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -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,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")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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?
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
module Card::Broadcastable
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
broadcasts_refreshes
|
||||
end
|
||||
end
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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) }
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -3,6 +3,8 @@ class Step < ApplicationRecord
|
||||
|
||||
scope :completed, -> { where(completed: true) }
|
||||
|
||||
validates :content, presence: true
|
||||
|
||||
def completed?
|
||||
completed
|
||||
end
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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">← <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">
|
||||
|
||||
@@ -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">← <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 doesn’t 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 doesn’t 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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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 %>
|
||||
@@ -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 }) %>
|
||||
@@ -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 %>
|
||||
|
||||
@@ -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 there’s 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 there’s no activity.
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
@@ -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, it’s 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? %>
|
||||
|
||||
<% 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>
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user