Merge pull request #585 from basecamp/entropy-settings-ui
Entropy improvements
This commit is contained in:
@@ -35,3 +35,4 @@
|
||||
/config/master.key
|
||||
|
||||
/config/credentials/*.key
|
||||
.DS_Store
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
@layer components {
|
||||
.divider {
|
||||
--divider-padding: var(--inline-space);
|
||||
--divider-color: var(--color-ink-light);
|
||||
|
||||
align-items: center;
|
||||
display: flex;
|
||||
margin-inline: calc(var(--divider-padding) * -1);
|
||||
|
||||
&:before,
|
||||
&:after {
|
||||
background: var(--divider-color);
|
||||
block-size: var(--divider-size, 1px);
|
||||
content: "";
|
||||
flex: 1;
|
||||
margin: 0 var(--divider-padding);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
@layer components {
|
||||
.knob {
|
||||
--knob-angle-reserve: 120deg;
|
||||
--knob-option-angle: calc((360deg - var(--knob-angle-reserve)) / (var(--knob-options) - 1));
|
||||
--knob-option-size: 3ch;
|
||||
--knob-chamfer-size: 1ch;
|
||||
--knob-color: oklch(var(--lch-ink-light));
|
||||
--knob-color-accent: oklch(var(--lch-blue-medium));
|
||||
--knob-tick-size: 1ch;
|
||||
--knob-radius: calc(var(--knob-size) / 2);
|
||||
--knob-size: 96px;
|
||||
|
||||
border: none;
|
||||
display: block;
|
||||
font-weight: 500;
|
||||
padding: var(--knob-option-size) 0 0;
|
||||
position: relative;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.knob__slider {
|
||||
--hover-size: 0;
|
||||
|
||||
appearance: none;
|
||||
background-color: transparent;
|
||||
block-size: var(--knob-size);
|
||||
inline-size: var(--knob-size);
|
||||
inset: 50% auto auto 50%;
|
||||
position: absolute;
|
||||
translate: -50% -50%;
|
||||
z-index: 1;
|
||||
|
||||
&::-webkit-slider-runnable-track {
|
||||
block-size: var(--knob-size);
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
&::-webkit-slider-thumb {
|
||||
appearance: none;
|
||||
background-color: transparent;
|
||||
height: 1px;
|
||||
width: 1px;
|
||||
}
|
||||
}
|
||||
|
||||
.knob__option {
|
||||
block-size: var(--knob-option-size);
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
display: grid;
|
||||
inline-size: var(--knob-option-size);
|
||||
inset: 50% auto auto 50%;
|
||||
place-content: center;
|
||||
position: absolute;
|
||||
transform:
|
||||
translate(-50%, -50%)
|
||||
rotate(calc(-1 * ((360deg - var(--knob-angle-reserve)) / 2) + (var(--knob-option-angle) * var(--i))))
|
||||
translateY(calc(-1 * var(--knob-radius) - 50% - var(--knob-tick-size)));
|
||||
z-index: 1;
|
||||
|
||||
&:hover,
|
||||
&:has(input:checked) {
|
||||
color: var(--knob-color-accent);
|
||||
}
|
||||
|
||||
&:has(:focus-visible) {
|
||||
box-shadow: 0 0 0 2px var(--knob-color);
|
||||
}
|
||||
|
||||
/* Tick marks */
|
||||
&:before {
|
||||
background-color: var(--knob-color);
|
||||
block-size: var(--knob-tick-size);
|
||||
content: "";
|
||||
inline-size: 2px;
|
||||
inset: 100% auto auto 50%;
|
||||
position: absolute;
|
||||
translate: -50% 0;
|
||||
}
|
||||
|
||||
/* The value text */
|
||||
span {
|
||||
rotate: calc(((360deg - var(--knob-angle-reserve)) / 2) - (var(--knob-option-angle) * var(--i)));
|
||||
}
|
||||
|
||||
input {
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
}
|
||||
}
|
||||
|
||||
.knob__knob {
|
||||
background: linear-gradient(to top, var(--knob-color), color-mix(in oklch, var(--knob-color) 50%, var(--color-canvas) 50%));
|
||||
block-size: var(--knob-size);
|
||||
border-radius: 50%;
|
||||
box-shadow:
|
||||
0 0 2px 1px rgba(0,0,0,0.10),
|
||||
0 2px 4px rgba(0,0,0,0.15),
|
||||
0 2px 8px rgba(0,0,0,0.20);
|
||||
inline-size: var(--knob-size);
|
||||
margin-inline: auto;
|
||||
position: relative;
|
||||
|
||||
&:before,
|
||||
&:after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
/* Indent */
|
||||
&:before {
|
||||
background: linear-gradient(to bottom, var(--knob-color), color-mix(in oklch, var(--knob-color) 50%, var(--color-canvas) 50%));
|
||||
block-size: calc(var(--knob-size) - var(--knob-chamfer-size));
|
||||
border-radius: 50%;
|
||||
box-shadow:
|
||||
0 -1px 0 rgba(255, 255, 255, 0.25),
|
||||
inset 0 -1px 0 rgba(255, 255, 255, 0.25);
|
||||
inline-size: calc(var(--knob-size) - var(--knob-chamfer-size));
|
||||
inset: 50% auto auto 50%;
|
||||
translate: -50% -50%;
|
||||
}
|
||||
|
||||
/* Indicator */
|
||||
&:after {
|
||||
background-color: var(--color-ink);
|
||||
block-size: calc(var(--knob-radius) - var(--knob-chamfer-size) / 2);
|
||||
border-radius: 50% 50% 2px 2px;
|
||||
inline-size: 4px;
|
||||
inset: auto auto 50% 50%;
|
||||
rotate: calc(-1 * ((360deg - var(--knob-angle-reserve)) / 2) + (var(--knob-option-angle) * var(--knob-index)));
|
||||
transform-origin: center bottom;
|
||||
transition: rotate 100ms;
|
||||
translate: -50% 0;
|
||||
}
|
||||
}
|
||||
|
||||
.knob__label {
|
||||
font-weight: bold;
|
||||
margin-block-start: 1ch;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
class Accounts::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_close_period, :auto_reconsider_period ])
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,6 @@
|
||||
class Accounts::SettingsController < ApplicationController
|
||||
def show
|
||||
@account = Account.sole
|
||||
@users = User.active
|
||||
end
|
||||
end
|
||||
@@ -33,7 +33,7 @@ class CollectionsController < ApplicationController
|
||||
end
|
||||
|
||||
def collection_params
|
||||
params.expect(collection: [ :name, :all_access, :auto_close_period ])
|
||||
params.expect(collection: [ :name, :all_access, :auto_close_period, :auto_reconsider_period ])
|
||||
end
|
||||
|
||||
def grantees
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
module UserTimelineScoped
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
include FilterScoped
|
||||
end
|
||||
|
||||
def show
|
||||
@filter = Current.user.filters.new(creator_ids: [ @user.id ])
|
||||
@day_timeline = Current.user.timeline_for(Time.current, filter: @filter)
|
||||
end
|
||||
end
|
||||
@@ -1,15 +1,10 @@
|
||||
class UsersController < ApplicationController
|
||||
include UserTimelineScoped
|
||||
require_unauthenticated_access only: %i[ new create ]
|
||||
|
||||
before_action :set_user, only: %i[ show edit update destroy ]
|
||||
before_action :ensure_join_code_is_valid, only: %i[ new create ]
|
||||
before_action :ensure_permission_to_change_user, only: %i[ update destroy ]
|
||||
|
||||
def index
|
||||
@users = User.active
|
||||
end
|
||||
|
||||
def new
|
||||
@user = User.new
|
||||
end
|
||||
@@ -23,6 +18,11 @@ class UsersController < ApplicationController
|
||||
def edit
|
||||
end
|
||||
|
||||
def show
|
||||
@filter = Current.user.filters.new(creator_ids: [ @user.id ])
|
||||
@day_timeline = Current.user.timeline_for(Time.current, filter: @filter)
|
||||
end
|
||||
|
||||
def update
|
||||
@user.update! user_params
|
||||
redirect_to @user
|
||||
|
||||
@@ -56,4 +56,8 @@ module CardsHelper
|
||||
"Closes"
|
||||
end
|
||||
end
|
||||
|
||||
def cacheable_preview_parts_for(card)
|
||||
[ card, card.collection.workflow, Entropy::Configuration.default ]
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
module CollectionsHelper
|
||||
def collection_auto_close_options
|
||||
[
|
||||
[ "3 days", 3.days ],
|
||||
[ "7 days", 7.days ],
|
||||
[ "30 days", 30.days ],
|
||||
[ "90 days", 90.days ],
|
||||
[ "365 days", 365.days ]
|
||||
]
|
||||
end
|
||||
|
||||
def collection_stalled_options
|
||||
[
|
||||
[ "1 day", 1.days ],
|
||||
[ "2 days", 2.days ],
|
||||
[ "3 days", 3.days ],
|
||||
[ "7 days", 7.days ],
|
||||
[ "14 days", 14.days ],
|
||||
[ "30 days", 30.days ],
|
||||
[ "365 days", 365.days ]
|
||||
]
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,5 @@
|
||||
module EntropyHelper
|
||||
def entropy_auto_close_options
|
||||
[ 3, 7, 11, 30, 90, 365 ]
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,38 @@
|
||||
import { Controller } from "@hotwired/stimulus"
|
||||
|
||||
export default class extends Controller {
|
||||
static targets = [ "field", "option", "slider" ]
|
||||
|
||||
connect() {
|
||||
this.#index = this.#selectedOption.dataset.index
|
||||
}
|
||||
|
||||
optionChanged({ target }) {
|
||||
this.#index = target.dataset.index
|
||||
}
|
||||
|
||||
sliderChanged({ target }) {
|
||||
this.#index = target.value
|
||||
}
|
||||
|
||||
set #index(index) {
|
||||
this.fieldTarget.style.setProperty("--knob-index", `${index}`);
|
||||
this.sliderTarget.value = index
|
||||
}
|
||||
|
||||
get #selectedOption() {
|
||||
return this.optionTargets.find(option => {
|
||||
return option.checked
|
||||
})
|
||||
}
|
||||
|
||||
set #value(index) {
|
||||
this.#optionForIndex(index).checked = true
|
||||
}
|
||||
|
||||
#optionForIndex(index) {
|
||||
return this.optionTargets.find(option => {
|
||||
return option.dataset.index === index;
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
class Account < ApplicationRecord
|
||||
include Joinable
|
||||
include Entropic, Joinable
|
||||
|
||||
has_many_attached :uploads
|
||||
end
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
module Account::Entropic
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
has_one :default_entropy_configuration, class_name: "Entropy::Configuration", as: :container, dependent: :destroy
|
||||
|
||||
before_save :set_default_entropy_configuration
|
||||
end
|
||||
|
||||
private
|
||||
DEFAULT_ENTROPY_PERIOD = 30.days
|
||||
|
||||
def set_default_entropy_configuration
|
||||
self.default_entropy_configuration ||= build_default_entropy_configuration \
|
||||
auto_close_period: DEFAULT_ENTROPY_PERIOD,
|
||||
auto_reconsider_period: DEFAULT_ENTROPY_PERIOD
|
||||
end
|
||||
end
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
class Card < ApplicationRecord
|
||||
include Assignable, Colored, Engageable, Entropy, Eventable,
|
||||
include Assignable, Colored, Engageable, Entropic, Eventable,
|
||||
Golden, Mentions, Pinnable, Closeable, Readable, Searchable,
|
||||
Staged, Statuses, Taggable, Watchable
|
||||
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
module Card::Entropic
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
ENTROPY_REMINDER_BEFORE = 7.days
|
||||
|
||||
included do
|
||||
scope :entropic_by, ->(period_name) do
|
||||
left_outer_joins(collection: :entropy_configuration)
|
||||
.where("last_active_at <= DATETIME('now', '-' || COALESCE(entropy_configurations.#{period_name}, ?) || ' seconds')",
|
||||
Entropy::Configuration.default.public_send(period_name))
|
||||
end
|
||||
|
||||
scope :stagnated, -> { doing.entropic_by(:auto_reconsider_period) }
|
||||
scope :due_to_be_closed, -> { considering.entropic_by(:auto_close_period) }
|
||||
delegate :auto_close_period, :auto_reconsider_period, to: :collection
|
||||
end
|
||||
|
||||
class_methods do
|
||||
def auto_close_all_due
|
||||
due_to_be_closed.find_each do |card|
|
||||
card.close(user: User.system, reason: "Closed")
|
||||
end
|
||||
end
|
||||
|
||||
def auto_reconsider_all_stagnated
|
||||
stagnated.find_each(&:reconsider)
|
||||
end
|
||||
end
|
||||
|
||||
def entropy
|
||||
Card::Entropy.for(self)
|
||||
end
|
||||
|
||||
def entropic?
|
||||
entropy.present?
|
||||
end
|
||||
end
|
||||
+14
-49
@@ -1,59 +1,24 @@
|
||||
module Card::Entropy
|
||||
extend ActiveSupport::Concern
|
||||
class Card::Entropy
|
||||
attr_reader :card, :auto_clean_period
|
||||
|
||||
AUTO_RECONSIDER_PERIOD = 30.days
|
||||
ENTROPY_REMINDER_BEFORE = 7.days
|
||||
class << self
|
||||
def for(card)
|
||||
return unless card.last_active_at
|
||||
|
||||
included do
|
||||
scope :in_auto_closing_collection, -> { joins(:collection).merge(Collection.auto_closing) }
|
||||
|
||||
scope :stagnated, -> { doing.where(last_active_at: ..AUTO_RECONSIDER_PERIOD.ago) }
|
||||
scope :due_to_be_closed, -> { considering.in_auto_closing_collection.where("last_active_at <= DATETIME('now', '-' || auto_close_period || ' seconds')") }
|
||||
|
||||
delegate :auto_close_period, to: :collection
|
||||
end
|
||||
|
||||
class_methods do
|
||||
def auto_close_all_due
|
||||
due_to_be_closed.find_each do |card|
|
||||
card.close(user: User.system, reason: "Closed")
|
||||
if card.considering?
|
||||
new(card, card.auto_close_period)
|
||||
elsif card.doing?
|
||||
new(card, card.auto_reconsider_period)
|
||||
end
|
||||
end
|
||||
|
||||
def auto_reconsider_all_stagnated
|
||||
stagnated.find_each(&:reconsider)
|
||||
end
|
||||
end
|
||||
|
||||
def subject_to_entropy?
|
||||
auto_reconsidering? || auto_closing?
|
||||
def initialize(card, auto_clean_period)
|
||||
@card = card
|
||||
@auto_clean_period = auto_clean_period
|
||||
end
|
||||
|
||||
def auto_reconsidering?
|
||||
doing? && last_active_at
|
||||
end
|
||||
|
||||
def auto_closing?
|
||||
considering? && collection.auto_closing? && last_active_at
|
||||
end
|
||||
|
||||
def auto_close_at
|
||||
last_active_at + auto_close_period if auto_closing?
|
||||
end
|
||||
|
||||
def days_until_close
|
||||
(auto_close_at.to_date - Date.current).to_i if auto_close_at
|
||||
end
|
||||
|
||||
def auto_reconsider_at
|
||||
last_active_at + AUTO_RECONSIDER_PERIOD if auto_reconsidering?
|
||||
end
|
||||
|
||||
def days_until_reconsider
|
||||
(auto_reconsider_at.to_date - Date.current).to_i if auto_reconsider_at
|
||||
end
|
||||
|
||||
def entropy_cleaned_at
|
||||
auto_close_at || auto_reconsider_at
|
||||
def auto_clean_at
|
||||
card.last_active_at + auto_clean_period
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
class Collection < ApplicationRecord
|
||||
include AutoClosing, Accessible, Broadcastable, Filterable, Workflowing
|
||||
include AutoClosing, Accessible, Broadcastable, Entropic, Filterable, Workflowing
|
||||
|
||||
belongs_to :creator, class_name: "User", default: -> { Current.user }
|
||||
|
||||
has_many :cards, dependent: :destroy
|
||||
has_many :tags, -> { distinct }, through: :cards
|
||||
has_many :events
|
||||
has_one :entropy_configuration, class_name: "Entropy::Configuration", as: :container, dependent: :destroy
|
||||
|
||||
scope :alphabetically, -> { order("lower(name)") }
|
||||
|
||||
|
||||
@@ -2,14 +2,9 @@ module Collection::AutoClosing
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
scope :auto_closing, -> { where.not(auto_close_period: nil) }
|
||||
before_create :set_default_auto_close_period
|
||||
end
|
||||
|
||||
def auto_closing?
|
||||
auto_close_period.present?
|
||||
end
|
||||
|
||||
private
|
||||
DEFAULT_AUTO_CLOSE_PERIOD = 30.days
|
||||
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
module Collection::Entropic
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
delegate :auto_close_period, :auto_reconsider_period, to: :entropy_configuration
|
||||
end
|
||||
|
||||
def entropy_configuration
|
||||
super || Entropy::Configuration.default
|
||||
end
|
||||
|
||||
def auto_close_period=(new_value)
|
||||
entropy_configuration ||= association(:entropy_configuration).reader || self.build_entropy_configuration
|
||||
entropy_configuration.update auto_close_period: new_value
|
||||
end
|
||||
|
||||
def auto_reconsider_period=(new_value)
|
||||
entropy_configuration ||= association(:entropy_configuration).reader || self.build_entropy_configuration
|
||||
entropy_configuration.update auto_reconsider_period: new_value
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,5 @@
|
||||
module Entropy
|
||||
def self.table_name_prefix
|
||||
"entropy_"
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,9 @@
|
||||
class Entropy::Configuration < ApplicationRecord
|
||||
belongs_to :container, polymorphic: true
|
||||
|
||||
class << self
|
||||
def default
|
||||
Account.sole.default_entropy_configuration
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,4 +1,4 @@
|
||||
<div class="flex flex-column align-center gap">
|
||||
<div class="flex flex-column align-center fill-shade pad border-radius gap">
|
||||
<% url = join_url(Account.sole.join_code) %>
|
||||
|
||||
<label class="flex flex-column gap full-width txt-align-center">
|
||||
@@ -0,0 +1,5 @@
|
||||
<div class="panel shadow center flex flex-column">
|
||||
<strong class="divider txt-large">Do or Die</strong>
|
||||
<p class="margin-none-block-start">Choose default settings for this account. You can override them in individual collections.</p>
|
||||
<%= render "entropy/auto_close", model: account.default_entropy_configuration, url: account_entropy_configuration_path %>
|
||||
</div>
|
||||
@@ -0,0 +1,7 @@
|
||||
<div class="panel shadow center flex flex-column">
|
||||
<strong class="divider txt-large margin-block-end">People on the account</strong>
|
||||
<%= render "accounts/invite" %>
|
||||
<div class="flex flex-column gap margin-block-start">
|
||||
<%= render partial: "accounts/user", collection: users %>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,21 @@
|
||||
<% @page_title = "Account Settings" %>
|
||||
|
||||
<% content_for :header do %>
|
||||
<nav>
|
||||
<%= link_to_back fallback_path: root_path %>
|
||||
|
||||
<header class="center">
|
||||
<h1 class="txt-large margin-none"><%= @page_title %></h1>
|
||||
</header>
|
||||
|
||||
<%= link_to workflows_path, class: "btn flex-item-justify-end" do %>
|
||||
<%= icon_tag "bolt" %>
|
||||
<span class="for-screen-reader">Workflows</span>
|
||||
<% end %>
|
||||
</nav>
|
||||
<% end %>
|
||||
|
||||
<section class="panels--two-up margin-block">
|
||||
<%= render "accounts/settings/users", users: @users %>
|
||||
<%= render "accounts/settings/entropy_configuration", account: @account %>
|
||||
</section>
|
||||
@@ -22,10 +22,12 @@
|
||||
</details>
|
||||
|
||||
<span class="card-perma__closure-message">
|
||||
<% if card.doing? %>
|
||||
Falls back to <em>Considering</em> if no activity <%= local_datetime_tag(card.auto_reconsider_at, style: :indays) -%>.
|
||||
<% elsif card.auto_closing? %>
|
||||
Auto-closes if no activity <%= local_datetime_tag(card.auto_close_at, style: :indays) -%>.
|
||||
<% if card.entropic? %>
|
||||
<% if card.doing? %>
|
||||
Falls back to <em>Considering</em> if no activity <%= local_datetime_tag(card.entropy.auto_clean_at, style: :indays) -%>.
|
||||
<% else %>
|
||||
Auto-closes if no activity <%= local_datetime_tag(card.entropy.auto_clean_at, style: :indays) -%>.
|
||||
<% end %>
|
||||
<% end %>
|
||||
</span>
|
||||
<% end %>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<% cache [ card, card.collection.workflow ] do %>
|
||||
<% cache cacheable_preview_parts_for(card) do %>
|
||||
<%= card_article_tag card, class: "card" do %>
|
||||
<div class="flex gap">
|
||||
<div class="flex flex-column flex-item-grow">
|
||||
@@ -29,7 +29,7 @@
|
||||
|
||||
<%= render "cards/display/common/background", card: card %>
|
||||
|
||||
<% if card.subject_to_entropy? %>
|
||||
<% if card.entropic? %>
|
||||
<%= render "cards/display/preview/bubble", card: card %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<div hidden class="bubble bubble--<%= card.engagement_status %>"
|
||||
data-controller="bubble" data-action="turbo:morph-element->bubble#update"
|
||||
data-bubble-reminder-before-value="<%= Card::ENTROPY_REMINDER_BEFORE.in_days.to_i %>"
|
||||
data-bubble-closes-at-value="<%= card.entropy_cleaned_at.iso8601 %>"
|
||||
data-bubble-reminder-before-value="<%= Card::Entropic::ENTROPY_REMINDER_BEFORE.in_days.to_i %>"
|
||||
data-bubble-closes-at-value="<%= card.entropy.auto_clean_at.iso8601 %>"
|
||||
data-bubble-entropy-action-value="<%= card_entropy_action(card) %>">
|
||||
<svg viewBox="0 0 200 100">
|
||||
<path id="top-half" fill="transparent" d="M 20,100 A 80,80 0 0,1 180,100" />
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<% end %>
|
||||
|
||||
<% if page.used? %>
|
||||
<%= render partial: "cards/display/preview", collection: page.records, as: :card, cached: ->(record) { [ record, record.collection.workflow ] } %>
|
||||
<%= render partial: "cards/display/preview", collection: page.records, as: :card, cached: ->(card) { cacheable_preview_parts_for(card) } %>
|
||||
|
||||
<% unless page.last? %>
|
||||
<%= cards_next_page_link "doing-cards", page: page, filter: filter %>
|
||||
|
||||
@@ -32,11 +32,9 @@
|
||||
|
||||
<div class="panel shadow center flex flex-column">
|
||||
<%= render "collections/edit/workflows", collection: @collection %>
|
||||
|
||||
<%= render "collections/edit/auto_close", collection: @collection %>
|
||||
|
||||
<%= render "collections/edit/stalled", collection: @collection %>
|
||||
<hr class="separator--horizontal full-width margin-block" style="--border-color: var(--color-ink-medium);" aria-hidden="true" />
|
||||
<hr class="separator--horizontal full-width margin-block-end margin-block-start-double" style="--border-color: var(--color-ink-light);" aria-hidden="true" />
|
||||
|
||||
<%= form_with model: @collection, class: "txt-align-center", method: :delete do |form| %>
|
||||
<%= form.button class: "btn txt-negative borderless txt-small", data: { turbo_confirm: "Are you sure you want to permanently delete this Collection?" } do %>
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
<strong class="txt-large">Do or Die</strong>
|
||||
<label for="closure" class="margin-block-end">Cards automatically close when there is no activity for…</label>
|
||||
|
||||
<%= form_with model: collection, data: { controller: "form" } do |form| %>
|
||||
<%= form.select :auto_close_period, collection_auto_close_options, {}, { class: "input input--select full-width margin-block-end", data: { action: "change->form#submit" } } %>
|
||||
<% end %>
|
||||
<div class="margin-block-end">
|
||||
<strong class="divider txt-large">Do or Die</strong>
|
||||
<%= render "entropy/auto_close", model: collection %>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
<% name = label.parameterize %>
|
||||
|
||||
<fieldset class="knob" data-controller="knob" data-knob-target="field" style="--knob-options: <%= knob_options.length %>; --knob-index: <%= default_index %>;">
|
||||
|
||||
<div class="position-relative" role="radiogroup" aria-labelledby="<%= name %>">
|
||||
<% knob_options.each_with_index do |value, i| %>
|
||||
<label class="knob__option" style="--i: <%= i %>">
|
||||
<input
|
||||
<%= "checked" if value == knob_options[default_index] %>
|
||||
data-action="change->knob#optionChanged"
|
||||
data-index="<%= i %>"
|
||||
data-knob-target="option"
|
||||
name="<%= name %>"
|
||||
type="radio"
|
||||
value="<%= value %>"
|
||||
/>
|
||||
<span><%= value %></span>
|
||||
</label>
|
||||
<% end %>
|
||||
<input
|
||||
aria-hidden
|
||||
class="knob__slider"
|
||||
data-action="input->knob#sliderChanged"
|
||||
data-knob-target="slider"
|
||||
max="<%= knob_options.length - 1 %>"
|
||||
min="0"
|
||||
type="range"
|
||||
value="<%= default_index %>"
|
||||
/>
|
||||
|
||||
<div class="knob__knob" aria-hidden></div>
|
||||
</div>
|
||||
|
||||
<div id="<%= name %>" class="knob__label">Days</div>
|
||||
</fieldset>
|
||||
@@ -1,6 +0,0 @@
|
||||
<strong class="txt-large">Stalled</strong>
|
||||
<label for="closure" class="margin-block-end">Mark cards stalled when there is no activity for…</label>
|
||||
|
||||
<%= form_with model: collection, data: { controller: "form" } do |form| %>
|
||||
<%= form.select :stalled_period, collection_stalled_options, {}, { class: "input input--select full-width margin-block-end", data: { action: "change->form#submit" } } %>
|
||||
<% end %>
|
||||
@@ -0,0 +1,29 @@
|
||||
<% url = local_assigns[:url] %>
|
||||
|
||||
<div class="flex align-start gap">
|
||||
<div class="flex flex-column full-width" style="flex-basis: 48%">
|
||||
<strong>Cards in Considering</strong>
|
||||
<p class="txt-x-small margin-none-block-start">Auto-close after…</p>
|
||||
<%= form_with model: model, url: url, data: { controller: "form" } do |form| %>
|
||||
<%= render "entropy/knob",
|
||||
form: form,
|
||||
name: :auto_close_period,
|
||||
knob_options: entropy_auto_close_options,
|
||||
label: "Days until auto-close" %>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<div class="separator margin-block-half" style="--border-color: var(--color-ink-light); min-block-size: 3lh;"></div>
|
||||
|
||||
<div class="flex flex-column full-width" style="flex-basis: 48%">
|
||||
<strong>Cards in Doing</strong>
|
||||
<p class="txt-x-small margin-none-block-start">Fall back to Considering after…</p>
|
||||
<%= form_with model: model, url: url, data: { controller: "form" } do |form| %>
|
||||
<%= render "entropy/knob",
|
||||
form: form,
|
||||
name: :auto_reconsider_period,
|
||||
knob_options: entropy_auto_close_options,
|
||||
label: "Days until auto-close" %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,33 @@
|
||||
<%
|
||||
period = form.object.send(name)
|
||||
current_index = knob_options.index(period.to_i / 1.day)
|
||||
%>
|
||||
|
||||
<fieldset class="knob" data-controller="knob" data-knob-target="field" style="--knob-options: <%= knob_options.length %>; --knob-index: <%= current_index %>">
|
||||
<div class="position-relative" role="radiogroup" aria-labelledby="<%= dom_id(form.object, name) %>">
|
||||
<% knob_options.each_with_index do |value, index| %>
|
||||
<label class="knob__option" style="--i: <%= index %>">
|
||||
<%= form.radio_button name,
|
||||
value.days,
|
||||
data: {
|
||||
action: "change->knob#optionChanged change->form#submit",
|
||||
index: index,
|
||||
knob_target: "option"
|
||||
} %>
|
||||
<span><%= value %></span>
|
||||
</label>
|
||||
<% end %>
|
||||
|
||||
<%= form.range_field :slider,
|
||||
class: "knob__slider",
|
||||
data: { action: "input->knob#sliderChanged change->form#submit", knob_target: "slider" },
|
||||
"aria-hidden": true,
|
||||
max: knob_options.length - 1,
|
||||
min: 0
|
||||
%>
|
||||
|
||||
<div class="knob__knob" aria-hidden></div>
|
||||
</div>
|
||||
|
||||
<div id="<%= dom_id(form.object, name) %>" class="knob__label">Days</div>
|
||||
</fieldset>
|
||||
@@ -22,7 +22,7 @@
|
||||
<%= render "events/filter", filter: @filter %>
|
||||
</div>
|
||||
|
||||
<%= link_to users_path, class: "btn flex-item-justify-end" do %>
|
||||
<%= link_to account_settings_path, class: "btn flex-item-justify-end" do %>
|
||||
<%= icon_tag "settings" %>
|
||||
<span class="for-screen-reader">Account settings</span>
|
||||
<% end %>
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
<% @page_title = "People on the account" %>
|
||||
|
||||
<% content_for :header do %>
|
||||
<nav class="align-start">
|
||||
<%= link_to_home icon: "arrow-left", label: "Go back", class: "flex-item-justify-start" %>
|
||||
|
||||
<header class="flex flex-column center">
|
||||
<h1 class="txt-x-large"><%= @page_title %></h1>
|
||||
</header>
|
||||
|
||||
<%= link_to workflows_path, class: "btn flex-item-justify-end" do %>
|
||||
<%= icon_tag "bolt" %>
|
||||
<span class="for-screen-reader">Workflows</span>
|
||||
<% end %>
|
||||
</nav>
|
||||
<% end %>
|
||||
|
||||
<div class="panel borderless center pad fill-shade flex flex-column margin-block">
|
||||
<%= render "users/invite" %>
|
||||
</div>
|
||||
|
||||
<div class="panel borderless center pad fill-none flex flex-column gap">
|
||||
<%= render @users %>
|
||||
</div>
|
||||
+24
-25
@@ -1,5 +1,28 @@
|
||||
{
|
||||
"ignored_warnings": [
|
||||
{
|
||||
"warning_type": "SQL Injection",
|
||||
"warning_code": 0,
|
||||
"fingerprint": "1261c0a89b23c1d7386d684f41c26cc230fa9b5b00d062d7f8d4d1a15bde96a8",
|
||||
"check_name": "SQL",
|
||||
"message": "Possible SQL injection",
|
||||
"file": "app/models/card/entropic.rb",
|
||||
"line": 9,
|
||||
"link": "https://brakemanscanner.org/docs/warning_types/sql_injection/",
|
||||
"code": "left_outer_joins(:collection => :entropy_configuration).where(\"last_active_at <= DATETIME('now', '-' || COALESCE(entropy_configurations.#{period_name}, ?) || ' seconds')\", Entropy::Configuration.default.public_send(period_name))",
|
||||
"render_path": null,
|
||||
"location": {
|
||||
"type": "method",
|
||||
"class": "Card::Entropic",
|
||||
"method": null
|
||||
},
|
||||
"user_input": "period_name",
|
||||
"confidence": "Weak",
|
||||
"cwe_id": [
|
||||
89
|
||||
],
|
||||
"note": ""
|
||||
},
|
||||
{
|
||||
"warning_type": "SQL Injection",
|
||||
"warning_code": 0,
|
||||
@@ -7,7 +30,7 @@
|
||||
"check_name": "SQL",
|
||||
"message": "Possible SQL injection",
|
||||
"file": "app/models/concerns/searchable.rb",
|
||||
"line": 14,
|
||||
"line": 22,
|
||||
"link": "https://brakemanscanner.org/docs/warning_types/sql_injection/",
|
||||
"code": "joins(\"join #{using} idx on #{table_name}.id = idx.rowid\")",
|
||||
"render_path": null,
|
||||
@@ -23,30 +46,6 @@
|
||||
],
|
||||
"note": ""
|
||||
},
|
||||
{
|
||||
"warning_type": "Dangerous Eval",
|
||||
"warning_code": 13,
|
||||
"fingerprint": "971ca740ceee7ae9a7d02a257964afd0b80672b3a4c49eeaf8a07a178bb84e78",
|
||||
"check_name": "Evaluation",
|
||||
"message": "Dynamic string evaluated as code",
|
||||
"file": "lib/rails_ext/action_text_has_markdown.rb",
|
||||
"line": 7,
|
||||
"link": "https://brakemanscanner.org/docs/warning_types/dangerous_eval/",
|
||||
"code": "class_eval(\" def #{name}\\n markdown_#{name} || build_markdown_#{name}\\n end\\n\\n def #{name}_html\\n #{name}.to_html\\n end\\n\\n def #{name}_plain_text\\n #{name}.to_plain_text\\n end\\n\\n def #{name}?\\n markdown_#{name}.present?\\n end\\n\\n def #{name}=(content)\\n self.#{name}.content = content\\n end\\n\", \"lib/rails_ext/action_text_has_markdown.rb\", 8)",
|
||||
"render_path": null,
|
||||
"location": {
|
||||
"type": "method",
|
||||
"class": "ActionText::HasMarkdown",
|
||||
"method": "has_markdown"
|
||||
},
|
||||
"user_input": null,
|
||||
"confidence": "Weak",
|
||||
"cwe_id": [
|
||||
913,
|
||||
95
|
||||
],
|
||||
"note": ""
|
||||
},
|
||||
{
|
||||
"warning_type": "Remote Code Execution",
|
||||
"warning_code": 24,
|
||||
|
||||
@@ -3,6 +3,11 @@ Rails.application.routes.draw do
|
||||
|
||||
resource :account do
|
||||
resource :join_code, module: :accounts
|
||||
|
||||
scope module: :accounts do
|
||||
resource :settings
|
||||
resource :entropy_configuration
|
||||
end
|
||||
end
|
||||
|
||||
resources :users do
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
class CreateEntropyConfigurations < ActiveRecord::Migration[8.1]
|
||||
def change
|
||||
create_table :entropy_configurations do |t|
|
||||
t.references :container, polymorphic: true, null: false, index: { unique: true }
|
||||
|
||||
t.bigint :auto_close_period, null: false, default: 30.days.to_i
|
||||
t.bigint :auto_reconsider_period, null: false, default: 30.days.to_i
|
||||
|
||||
t.timestamps
|
||||
|
||||
t.index %i[ container_type container_id auto_close_period ]
|
||||
t.index %i[ container_type container_id auto_reconsider_period ]
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,5 @@
|
||||
class RemoveAutoClosePeriodFromCollections < ActiveRecord::Migration[8.1]
|
||||
def change
|
||||
remove_column :collections, :auto_close_period
|
||||
end
|
||||
end
|
||||
Generated
+13
-3
@@ -10,7 +10,7 @@
|
||||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema[8.1].define(version: 2025_06_01_161653) do
|
||||
ActiveRecord::Schema[8.1].define(version: 2025_06_04_080022) do
|
||||
create_table "accesses", force: :cascade do |t|
|
||||
t.integer "collection_id", null: false
|
||||
t.datetime "created_at", null: false
|
||||
@@ -141,13 +141,11 @@ ActiveRecord::Schema[8.1].define(version: 2025_06_01_161653) do
|
||||
|
||||
create_table "collections", force: :cascade do |t|
|
||||
t.boolean "all_access", default: false, null: false
|
||||
t.bigint "auto_close_period"
|
||||
t.datetime "created_at", null: false
|
||||
t.integer "creator_id", null: false
|
||||
t.string "name", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.integer "workflow_id"
|
||||
t.index ["auto_close_period"], name: "index_collections_on_auto_close_period"
|
||||
t.index ["creator_id"], name: "index_collections_on_creator_id"
|
||||
t.index ["workflow_id"], name: "index_collections_on_workflow_id"
|
||||
end
|
||||
@@ -188,6 +186,18 @@ ActiveRecord::Schema[8.1].define(version: 2025_06_01_161653) do
|
||||
t.index ["filter_id"], name: "index_creators_filters_on_filter_id"
|
||||
end
|
||||
|
||||
create_table "entropy_configurations", force: :cascade do |t|
|
||||
t.bigint "auto_close_period", default: 2592000, null: false
|
||||
t.bigint "auto_reconsider_period", default: 2592000, null: false
|
||||
t.integer "container_id", null: false
|
||||
t.string "container_type", null: false
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.index ["container_type", "container_id", "auto_close_period"], name: "idx_on_container_type_container_id_auto_close_perio_74dc880875"
|
||||
t.index ["container_type", "container_id", "auto_reconsider_period"], name: "idx_on_container_type_container_id_auto_reconsider__583aaddbea"
|
||||
t.index ["container_type", "container_id"], name: "index_entropy_configurations_on_container", unique: true
|
||||
end
|
||||
|
||||
create_table "events", force: :cascade do |t|
|
||||
t.string "action", null: false
|
||||
t.integer "collection_id", null: false
|
||||
|
||||
+101
-27
@@ -495,16 +495,6 @@ columns:
|
||||
default_function:
|
||||
collation:
|
||||
comment:
|
||||
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
||||
auto_increment:
|
||||
name: auto_close_period
|
||||
cast_type: *11
|
||||
sql_type_metadata: *12
|
||||
'null': true
|
||||
default:
|
||||
default_function:
|
||||
collation:
|
||||
comment:
|
||||
- *5
|
||||
- *24
|
||||
- *6
|
||||
@@ -585,6 +575,50 @@ columns:
|
||||
creators_filters:
|
||||
- *24
|
||||
- *19
|
||||
entropy_configurations:
|
||||
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
||||
auto_increment:
|
||||
name: auto_close_period
|
||||
cast_type: *11
|
||||
sql_type_metadata: *12
|
||||
'null': false
|
||||
default: 2592000
|
||||
default_function:
|
||||
collation:
|
||||
comment:
|
||||
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
||||
auto_increment:
|
||||
name: auto_reconsider_period
|
||||
cast_type: *11
|
||||
sql_type_metadata: *12
|
||||
'null': false
|
||||
default: 2592000
|
||||
default_function:
|
||||
collation:
|
||||
comment:
|
||||
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
||||
auto_increment:
|
||||
name: container_id
|
||||
cast_type: *1
|
||||
sql_type_metadata: *2
|
||||
'null': false
|
||||
default:
|
||||
default_function:
|
||||
collation:
|
||||
comment:
|
||||
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
||||
auto_increment:
|
||||
name: container_type
|
||||
cast_type: *7
|
||||
sql_type_metadata: *8
|
||||
'null': false
|
||||
default:
|
||||
default_function:
|
||||
collation:
|
||||
comment:
|
||||
- *5
|
||||
- *6
|
||||
- *9
|
||||
events:
|
||||
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
||||
auto_increment:
|
||||
@@ -997,6 +1031,7 @@ primary_keys:
|
||||
commands: id
|
||||
comments: id
|
||||
creators_filters:
|
||||
entropy_configurations: id
|
||||
events: id
|
||||
filters: id
|
||||
filters_stages:
|
||||
@@ -1035,6 +1070,7 @@ data_sources:
|
||||
commands: true
|
||||
comments: true
|
||||
creators_filters: true
|
||||
entropy_configurations: true
|
||||
events: true
|
||||
filters: true
|
||||
filters_stages: true
|
||||
@@ -1447,22 +1483,6 @@ indexes:
|
||||
comment:
|
||||
valid: true
|
||||
collections:
|
||||
- !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition
|
||||
table: collections
|
||||
name: index_collections_on_auto_close_period
|
||||
unique: false
|
||||
columns:
|
||||
- auto_close_period
|
||||
lengths: {}
|
||||
orders: {}
|
||||
opclasses: {}
|
||||
where:
|
||||
type:
|
||||
using:
|
||||
include:
|
||||
nulls_not_distinct:
|
||||
comment:
|
||||
valid: true
|
||||
- !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition
|
||||
table: collections
|
||||
name: index_collections_on_creator_id
|
||||
@@ -1646,6 +1666,60 @@ indexes:
|
||||
nulls_not_distinct:
|
||||
comment:
|
||||
valid: true
|
||||
entropy_configurations:
|
||||
- !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition
|
||||
table: entropy_configurations
|
||||
name: idx_on_container_type_container_id_auto_close_perio_74dc880875
|
||||
unique: false
|
||||
columns:
|
||||
- container_type
|
||||
- container_id
|
||||
- auto_close_period
|
||||
lengths: {}
|
||||
orders: {}
|
||||
opclasses: {}
|
||||
where:
|
||||
type:
|
||||
using:
|
||||
include:
|
||||
nulls_not_distinct:
|
||||
comment:
|
||||
valid: true
|
||||
- !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition
|
||||
table: entropy_configurations
|
||||
name: idx_on_container_type_container_id_auto_reconsider__583aaddbea
|
||||
unique: false
|
||||
columns:
|
||||
- container_type
|
||||
- container_id
|
||||
- auto_reconsider_period
|
||||
lengths: {}
|
||||
orders: {}
|
||||
opclasses: {}
|
||||
where:
|
||||
type:
|
||||
using:
|
||||
include:
|
||||
nulls_not_distinct:
|
||||
comment:
|
||||
valid: true
|
||||
- !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition
|
||||
table: entropy_configurations
|
||||
name: index_entropy_configurations_on_container
|
||||
unique: true
|
||||
columns:
|
||||
- container_type
|
||||
- container_id
|
||||
lengths: {}
|
||||
orders: {}
|
||||
opclasses: {}
|
||||
where:
|
||||
type:
|
||||
using:
|
||||
include:
|
||||
nulls_not_distinct:
|
||||
comment:
|
||||
valid: true
|
||||
events:
|
||||
- !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition
|
||||
table: events
|
||||
@@ -2137,4 +2211,4 @@ indexes:
|
||||
comment:
|
||||
valid: true
|
||||
workflows: []
|
||||
version: 20250601161653
|
||||
version: 20250604080022
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
require "test_helper"
|
||||
|
||||
class Accounts::EntropyConfigurationsControllerTest < ActionDispatch::IntegrationTest
|
||||
setup do
|
||||
sign_in_as :kevin
|
||||
end
|
||||
|
||||
test "update" do
|
||||
put account_entropy_configuration_path, params: { entropy_configuration: { auto_close_period: 1.day, auto_reconsider_period: 2.days } }
|
||||
|
||||
assert_equal 1.day, entropy_configurations("37s_account").auto_close_period
|
||||
assert_equal 2.days, entropy_configurations("37s_account").auto_reconsider_period
|
||||
|
||||
assert_redirected_to account_settings_path
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,12 @@
|
||||
require "test_helper"
|
||||
|
||||
class Accounts::SettingsControllerTest < ActionDispatch::IntegrationTest
|
||||
setup do
|
||||
sign_in_as :kevin
|
||||
end
|
||||
|
||||
test "show" do
|
||||
get account_settings_path
|
||||
assert_response :success
|
||||
end
|
||||
end
|
||||
@@ -30,7 +30,9 @@ class CollectionsControllerTest < ActionDispatch::IntegrationTest
|
||||
patch collection_path(collections(:writebook)), params: {
|
||||
collection: {
|
||||
name: "Writebook bugs",
|
||||
all_access: false
|
||||
all_access: false,
|
||||
auto_close_period: 1.day,
|
||||
auto_reconsider_period: 2.days
|
||||
},
|
||||
user_ids: users(:david, :jz).pluck(:id)
|
||||
}
|
||||
@@ -38,6 +40,8 @@ class CollectionsControllerTest < ActionDispatch::IntegrationTest
|
||||
assert_redirected_to edit_collection_path(collections(:writebook))
|
||||
assert_equal "Writebook bugs", collections(:writebook).reload.name
|
||||
assert_equal users(:david, :jz).sort, collections(:writebook).users.sort
|
||||
assert_equal 1.day, entropy_configurations(:writebook_collection).auto_close_period
|
||||
assert_equal 2.days, entropy_configurations(:writebook_collection).auto_reconsider_period
|
||||
assert_not collections(:writebook).all_access?
|
||||
end
|
||||
|
||||
|
||||
@@ -1,14 +1,6 @@
|
||||
require "test_helper"
|
||||
|
||||
class UsersControllerTest < ActionDispatch::IntegrationTest
|
||||
test "index" do
|
||||
sign_in_as :kevin
|
||||
|
||||
get users_path
|
||||
assert_in_body users(:david).name
|
||||
assert_in_body users(:kevin).name
|
||||
end
|
||||
|
||||
test "new" do
|
||||
get new_user_path(params: { join_code: "bad" })
|
||||
assert_response :forbidden
|
||||
|
||||
Vendored
-2
@@ -2,11 +2,9 @@ writebook:
|
||||
name: Writebook
|
||||
creator: david
|
||||
all_access: true
|
||||
auto_close_period: <%= 30.days.to_i %>
|
||||
workflow: qa
|
||||
|
||||
private:
|
||||
name: Private collection
|
||||
creator: kevin
|
||||
all_access: false
|
||||
auto_close_period: <%= 30.days.to_i %>
|
||||
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
37s_account:
|
||||
container: 37s (Account)
|
||||
auto_reconsider_period: <%= 30.days.to_i %>
|
||||
auto_close_period: <%= 30.days.to_i %>
|
||||
|
||||
writebook_collection:
|
||||
container: writebook (Collection)
|
||||
auto_reconsider_period: <%= 90.days.to_i %>
|
||||
auto_close_period: <%= 90.days.to_i %>
|
||||
|
||||
private_collection:
|
||||
container: private (Collection)
|
||||
auto_reconsider_period: <%= 30.days.to_i %>
|
||||
auto_close_period: <%= 30.days.to_i %>
|
||||
@@ -0,0 +1,107 @@
|
||||
require "test_helper"
|
||||
|
||||
class Card::EntropicTest < ActiveSupport::TestCase
|
||||
setup do
|
||||
Current.session = sessions(:david)
|
||||
end
|
||||
|
||||
test "auto_close_at uses the period defined in the account by default" do
|
||||
freeze_time
|
||||
|
||||
entropy_configurations(:writebook_collection).destroy
|
||||
entropy_configurations("37s_account").reload.update! auto_close_period: 456.days
|
||||
cards(:layout).update! last_active_at: 2.day.ago
|
||||
assert_equal (456 - 2).days.from_now, cards(:layout).entropy.auto_clean_at
|
||||
end
|
||||
|
||||
test "auto_close_at infers the period from the collection when present" do
|
||||
freeze_time
|
||||
|
||||
entropy_configurations(:writebook_collection).update! auto_close_period: 123.days
|
||||
cards(:layout).update! last_active_at: 2.day.ago
|
||||
assert_equal (123 - 2).days.from_now, cards(:layout).entropy.auto_clean_at
|
||||
end
|
||||
|
||||
test "auto_reconsider_at uses the period defined in the account by default" do
|
||||
freeze_time
|
||||
|
||||
cards(:layout).engage
|
||||
entropy_configurations(:writebook_collection).destroy
|
||||
entropy_configurations("37s_account").reload.update! auto_reconsider_period: 456.days
|
||||
cards(:layout).update! last_active_at: 2.day.ago
|
||||
assert_equal (456 - 2).days.from_now, cards(:layout).entropy.auto_clean_at
|
||||
end
|
||||
|
||||
test "auto_reconsider_at infers the period from the collection when present" do
|
||||
freeze_time
|
||||
|
||||
cards(:layout).engage
|
||||
entropy_configurations(:writebook_collection).update! auto_reconsider_period: 123.days
|
||||
cards(:layout).update! last_active_at: 2.day.ago
|
||||
assert_equal (123 - 2).days.from_now, cards(:layout).entropy.auto_clean_at
|
||||
end
|
||||
|
||||
test "auto close all due using the default account entropy configuration" do
|
||||
cards(:logo, :shipping).each(&:reconsider)
|
||||
entropy_configurations(:writebook_collection).destroy
|
||||
|
||||
cards(:logo).update!(last_active_at: 1.day.ago - entropy_configurations("37s_account").auto_close_period)
|
||||
cards(:shipping).update!(last_active_at: 1.day.from_now - entropy_configurations("37s_account").auto_close_period)
|
||||
|
||||
assert_difference -> { Card.closed.count }, +1 do
|
||||
Card.auto_close_all_due
|
||||
end
|
||||
|
||||
assert cards(:logo).reload.closed?
|
||||
assert_not cards(:shipping).reload.closed?
|
||||
end
|
||||
|
||||
test "auto close all due using entropy configuration defined at the collection level" do
|
||||
cards(:logo, :shipping).each(&:reconsider)
|
||||
|
||||
cards(:logo).update!(last_active_at: 1.day.ago - entropy_configurations(:writebook_collection).auto_close_period)
|
||||
cards(:shipping).update!(last_active_at: 1.day.from_now - entropy_configurations(:writebook_collection).auto_close_period)
|
||||
|
||||
assert_difference -> { Card.closed.count }, +1 do
|
||||
Card.auto_close_all_due
|
||||
end
|
||||
|
||||
assert cards(:logo).reload.closed?
|
||||
assert_not cards(:shipping).reload.closed?
|
||||
end
|
||||
|
||||
test "auto consider all stagnated using the default account entropy configuration" do
|
||||
travel_to Time.current
|
||||
|
||||
cards(:logo, :shipping).each(&:engage)
|
||||
entropy_configurations(:writebook_collection).destroy
|
||||
|
||||
cards(:logo).update!(last_active_at: 1.day.ago - entropy_configurations("37s_account").auto_close_period)
|
||||
cards(:shipping).update!(last_active_at: 1.day.from_now - entropy_configurations("37s_account").auto_close_period)
|
||||
|
||||
assert_difference -> { Card.considering.count }, +1 do
|
||||
Card.auto_reconsider_all_stagnated
|
||||
end
|
||||
|
||||
assert cards(:shipping).reload.doing?
|
||||
assert cards(:logo).reload.considering?
|
||||
assert_equal Time.current, cards(:logo).last_active_at
|
||||
end
|
||||
|
||||
test "auto_reconsider_all_stagnated" do
|
||||
travel_to Time.current
|
||||
|
||||
cards(:logo, :shipping).each(&:engage)
|
||||
|
||||
cards(:logo).update!(last_active_at: 1.day.ago - entropy_configurations("writebook_collection").auto_close_period)
|
||||
cards(:shipping).update!(last_active_at: 1.day.from_now - entropy_configurations("writebook_collection").auto_close_period)
|
||||
|
||||
assert_difference -> { Card.considering.count }, +1 do
|
||||
Card.auto_reconsider_all_stagnated
|
||||
end
|
||||
|
||||
assert cards(:shipping).reload.doing?
|
||||
assert cards(:logo).reload.considering?
|
||||
assert_equal Time.current, cards(:logo).last_active_at
|
||||
end
|
||||
end
|
||||
@@ -1,66 +0,0 @@
|
||||
require "test_helper"
|
||||
|
||||
class Card::EntropyTest < ActiveSupport::TestCase
|
||||
setup do
|
||||
Current.session = sessions(:david)
|
||||
end
|
||||
|
||||
test "auto_close_at infers the period from the collection" do
|
||||
freeze_time
|
||||
|
||||
collections(:writebook).update! auto_close_period: 123.days
|
||||
cards(:layout).update! last_active_at: 2.day.ago
|
||||
assert_equal (123-2).days.from_now, cards(:layout).auto_close_at
|
||||
end
|
||||
|
||||
test "auto close all due" do
|
||||
cards(:logo, :shipping).each(&:reconsider)
|
||||
|
||||
cards(:logo).update!(last_active_at: 1.day.ago - collections(:writebook).auto_close_period)
|
||||
cards(:shipping).update!(last_active_at: 1.day.from_now - collections(:writebook).auto_close_period)
|
||||
|
||||
assert_difference -> { Card.closed.count }, +1 do
|
||||
Card.auto_close_all_due
|
||||
end
|
||||
|
||||
assert cards(:logo).reload.closed?
|
||||
assert_not cards(:shipping).reload.closed?
|
||||
end
|
||||
|
||||
test "don't auto close those cards where the collection has no auto close period" do
|
||||
cards(:logo, :shipping).each(&:reconsider)
|
||||
|
||||
collections(:writebook).update auto_close_period: nil
|
||||
|
||||
assert_no_difference -> { Card.closed.count } do
|
||||
Card.auto_close_all_due
|
||||
end
|
||||
|
||||
assert_not cards(:logo).reload.closed?
|
||||
end
|
||||
|
||||
test "auto_reconsider_all_stagnated" do
|
||||
travel_to Time.current
|
||||
|
||||
cards(:logo, :shipping).each(&:engage)
|
||||
|
||||
cards(:logo).update!(last_active_at: 1.day.ago - Card::AUTO_RECONSIDER_PERIOD)
|
||||
cards(:shipping).update!(last_active_at: 1.day.from_now - Card::AUTO_RECONSIDER_PERIOD)
|
||||
|
||||
assert_difference -> { Card.considering.count }, +1 do
|
||||
Card.auto_reconsider_all_stagnated
|
||||
end
|
||||
|
||||
assert cards(:shipping).reload.doing?
|
||||
assert cards(:logo).reload.considering?
|
||||
assert_equal Time.current, cards(:logo).last_active_at
|
||||
end
|
||||
|
||||
test "entropy_cleaned_at returns when the entropy will be cleaned" do
|
||||
assert_equal cards(:layout).auto_close_at, cards(:layout).entropy_cleaned_at
|
||||
assert_not_nil cards(:layout).entropy_cleaned_at
|
||||
|
||||
assert_equal cards(:logo).auto_reconsider_at, cards(:logo).entropy_cleaned_at
|
||||
assert_not_nil cards(:logo).entropy_cleaned_at
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user