Configure the autoclose period at the collection level
This commit is contained in:
@@ -6,7 +6,7 @@ class CollectionsController < ApplicationController
|
||||
end
|
||||
|
||||
def create
|
||||
@collection = Collection.create! collection_params
|
||||
@collection = Collection.create! collection_params.with_defaults(all_access: true)
|
||||
redirect_to cards_path(collection_ids: [ @collection ])
|
||||
end
|
||||
|
||||
@@ -19,7 +19,7 @@ class CollectionsController < ApplicationController
|
||||
@collection.update! collection_params
|
||||
@collection.accesses.revise granted: grantees, revoked: revokees
|
||||
|
||||
redirect_to cards_path(collection_ids: [ @collection ])
|
||||
redirect_to edit_collection_path(@collection), notice: "Collection updated"
|
||||
end
|
||||
|
||||
def destroy
|
||||
@@ -33,7 +33,7 @@ class CollectionsController < ApplicationController
|
||||
end
|
||||
|
||||
def collection_params
|
||||
params.expect(collection: [ :name, :all_access ]).with_defaults(all_access: true)
|
||||
params.expect(collection: [ :name, :all_access, :auto_close_period ])
|
||||
end
|
||||
|
||||
def grantees
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
module CollectionsHelper
|
||||
def collection_auto_close_options
|
||||
[
|
||||
[ "30 days", 30.days ],
|
||||
[ "60 days", 60.days ],
|
||||
[ "90 days", 90.days ],
|
||||
[ "6 months", 180.days ],
|
||||
[ "1 year", 365.days ],
|
||||
[ "Never", nil ]
|
||||
]
|
||||
end
|
||||
end
|
||||
@@ -1,17 +1,6 @@
|
||||
module Card::Closeable
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
AUTO_CLOSE_OPTIONS = [
|
||||
[ "30 days", 30.days ],
|
||||
[ "60 days", 60.days ],
|
||||
[ "90 days", 90.days ],
|
||||
[ "6 months", 180.days ],
|
||||
[ "1 year", 365.days ],
|
||||
[ "Never", nil ]
|
||||
].freeze
|
||||
|
||||
AUTO_CLOSE_AFTER = 30.days
|
||||
|
||||
included do
|
||||
has_one :closure, dependent: :destroy
|
||||
|
||||
@@ -19,7 +8,10 @@ module Card::Closeable
|
||||
scope :open, -> { where.missing(:closure) }
|
||||
|
||||
scope :recently_closed_first, -> { closed.order("closures.created_at": :desc) }
|
||||
scope :due_to_be_closed, -> { considering.where(last_active_at: ..AUTO_CLOSE_AFTER.ago) }
|
||||
scope :in_auto_closing_collection, -> { joins(:collection).merge(Collection.auto_closing) }
|
||||
scope :due_to_be_closed, -> { considering.in_auto_closing_collection.where("last_active_at <= DATETIME('now', '-' || auto_close_period || ' seconds')") }
|
||||
|
||||
delegate :auto_closing?, :auto_close_period, to: :collection
|
||||
end
|
||||
|
||||
class_methods do
|
||||
@@ -31,7 +23,7 @@ module Card::Closeable
|
||||
end
|
||||
|
||||
def auto_close_at
|
||||
last_active_at + AUTO_CLOSE_AFTER if last_active_at
|
||||
last_active_at + auto_close_period if auto_closing? && last_active_at
|
||||
end
|
||||
|
||||
def closed?
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
class Collection < ApplicationRecord
|
||||
include Accessible, Broadcastable, Filterable, Workflowing
|
||||
include AutoClosing, Accessible, Broadcastable, Filterable, Workflowing
|
||||
|
||||
belongs_to :creator, class_name: "User", default: -> { Current.user }
|
||||
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
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
|
||||
|
||||
def set_default_auto_close_period
|
||||
self.auto_close_period ||= DEFAULT_AUTO_CLOSE_PERIOD unless attribute_present?(:auto_close_period)
|
||||
end
|
||||
end
|
||||
@@ -24,7 +24,7 @@
|
||||
<span class="card-perma__closure-message">
|
||||
<% if card.doing? %>
|
||||
Returns to <em>Considering</em> if no activity <%= local_datetime_tag(card.auto_reconsider_at, style: :indays) -%>.
|
||||
<% else %>
|
||||
<% elsif card.auto_closing? %>
|
||||
Auto-closes if no activity <%= local_datetime_tag(card.auto_close_at, style: :indays) -%>.
|
||||
<% end %>
|
||||
</span>
|
||||
|
||||
@@ -17,56 +17,9 @@
|
||||
<strong class="txt-large">Name and Access</strong>
|
||||
<p class="margin-none-block-start">Choose who can access this Collection.</p>
|
||||
<%= form_with model: @collection, class: "flex flex-column gap txt-large", data: { controller: "form" } do |form| %>
|
||||
<div class="flex align-center gap">
|
||||
<label class="flex-item-grow">
|
||||
<strong><%= form.text_field :name, name: "collection[name]", class: "input full-width",
|
||||
required: true, autofocus: true, placeholder: "Give it a name…",
|
||||
data: { action: "keydown.enter->form#submit:prevent, keydown.esc->form#cancel" } %></strong>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<section class="margin-block fill-shade border-radius">
|
||||
<%= access_menu_tag @collection do %>
|
||||
<li class="flex align-center gap pad unpad-block-end txt-normal">
|
||||
<figure class="avatar flex-item-no-shrink" style="--avatar-border-radius: 0; --avatar-size: 3.5ch; margin-inline: 0.3em;">
|
||||
<%= icon_tag "everyone" %>
|
||||
<span class="for-screen-reader">Everyone</span>
|
||||
</figure>
|
||||
|
||||
<div class="min-width">
|
||||
<div class="overflow-ellipsis fill-shade"><strong>Everyone</strong></div>
|
||||
</div>
|
||||
|
||||
<hr class="separator--horizontal flex-item-grow" style="--border-color: var(--color-subtle-dark); --border-style: dashed" aria-hidden="true" />
|
||||
|
||||
<label for="collection_all_access" class="switch">
|
||||
<%= form.check_box :all_access, class: "switch__input", checked: @collection.all_access?, data: { action: "change->toggle-class#toggle" } %>
|
||||
<span class="switch__btn round"></span>
|
||||
<span class="for-screen-reader">Give everyone access to this project</span>
|
||||
</label>
|
||||
</li>
|
||||
|
||||
<hr class="separator--horizontal margin-inline" style="--border-color: var(--color-subtle-dark);" aria-hidden="true" />
|
||||
|
||||
<% if User.active.count > 20 %>
|
||||
<div class="pad-inline">
|
||||
<input placeholder="Filter…" class="input input--transparent full-width" type="search" autocorrect="off" autocomplete="off" data-1p-ignore="true" data-action="input->filter#filter">
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<div data-filter-target="list" class="users-list pad-inline">
|
||||
<% 20.times do %>
|
||||
<%= access_toggles_for @selected_users, selected: true %>
|
||||
<% end %>
|
||||
|
||||
<% if @selected_users.any? && @unselected_users.any? %>
|
||||
<hr class="separator full-width" style="--border-style: solid">
|
||||
<% end %>
|
||||
|
||||
<%= access_toggles_for @unselected_users, selected: false %>
|
||||
</div>
|
||||
<% end %>
|
||||
</section>
|
||||
<%= render "collections/edit/name", form: form %>
|
||||
<%= render "collections/edit/users", collection: @collection, selected_users: @selected_users, unselected_users: @unselected_users, form: form %>
|
||||
|
||||
<button type="submit" id="log_in" class="btn btn--reversed center txt-normal">
|
||||
<%= icon_tag "check" %>
|
||||
@@ -78,33 +31,10 @@
|
||||
</div>
|
||||
|
||||
<div class="panel shadow center flex flex-column">
|
||||
<strong class="txt-large">Auto-Close Cards</strong>
|
||||
<label for="closure" class="margin-block-end">Cards automatically close when there is no activity for…</label>
|
||||
<%= select_tag :closure, options_for_select(Card::Closeable::AUTO_CLOSE_OPTIONS, Card::Closeable::AUTO_CLOSE_AFTER), { class: "input input--select full-width margin-block-end", data: { action: "change->form#submit" } } %>
|
||||
<%= render "collections/edit/auto_close", collection: @collection %>
|
||||
|
||||
<hr class="separator--horizontal full-width margin-block" style="--border-color: var(--color-subtle);" aria-hidden="true" />
|
||||
|
||||
<strong class="txt-large">Workflows</strong>
|
||||
<div class="flex justify-center margin-block-end">Use a Workflow to track progress in this Collection.</div>
|
||||
<div class="flex flex-wrap gap">
|
||||
<% Workflow.all.each do |workflow| %>
|
||||
<div class="workflow-preview txt-align-start border border-radius <%= 'workflow-preview--selected' if workflow == @collection.workflow %>">
|
||||
<%= button_to collection_workflow_path(@collection), method: :patch do %>
|
||||
<%= hidden_field_tag "collection[workflow_id]", workflow.id %>
|
||||
<strong class="txt-small overflow-ellipsis"><%= workflow.name %></strong>
|
||||
<ul class="list-style-none txt-xx-small ">
|
||||
<% workflow.stages.each do |stage| %>
|
||||
<li class="overflow-ellipsis flex align-center gap-half min-width">
|
||||
<span class="workflow-preview__swatch btn btn--circle" style="--btn-background: <%= stage.color.present? ? stage.color : Card::Colored::DEFAULT_COLOR %>;"></span>
|
||||
<span class="overflow-ellipsis"><%= stage.name %></span>
|
||||
</li>
|
||||
<% end %>
|
||||
</ul>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<p><%= link_to "Manage workflows", workflows_path, class: "btn btn--plain txt-link" %></p>
|
||||
<%= render "collections/edit/workflows", collection: @collection %>
|
||||
|
||||
<hr class="separator--horizontal full-width" style="--border-color: var(--color-subtle); margin-block-start: auto;" aria-hidden="true" />
|
||||
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
<strong class="txt-large">Auto-Close Cards</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 %>
|
||||
@@ -0,0 +1,7 @@
|
||||
<div class="flex align-center gap">
|
||||
<label class="flex-item-grow">
|
||||
<strong><%= form.text_field :name, name: "collection[name]", class: "input full-width",
|
||||
required: true, autofocus: true, placeholder: "Give it a name…",
|
||||
data: { action: "keydown.enter->form#submit:prevent, keydown.esc->form#cancel" } %></strong>
|
||||
</label>
|
||||
</div>
|
||||
@@ -0,0 +1,42 @@
|
||||
<section class="margin-block fill-shade border-radius">
|
||||
<%= access_menu_tag collection do %>
|
||||
<li class="flex align-center gap pad unpad-block-end txt-normal">
|
||||
<figure class="avatar flex-item-no-shrink" style="--avatar-border-radius: 0; --avatar-size: 3.5ch; margin-inline: 0.3em;">
|
||||
<%= icon_tag "everyone" %>
|
||||
<span class="for-screen-reader">Everyone</span>
|
||||
</figure>
|
||||
|
||||
<div class="min-width">
|
||||
<div class="overflow-ellipsis fill-shade"><strong>Everyone</strong></div>
|
||||
</div>
|
||||
|
||||
<hr class="separator--horizontal flex-item-grow" style="--border-color: var(--color-subtle-dark); --border-style: dashed" aria-hidden="true" />
|
||||
|
||||
<label for="collection_all_access" class="switch">
|
||||
<%= form.check_box :all_access, class: "switch__input", checked: collection.all_access?, data: { action: "change->toggle-class#toggle" } %>
|
||||
<span class="switch__btn round"></span>
|
||||
<span class="for-screen-reader">Give everyone access to this project</span>
|
||||
</label>
|
||||
</li>
|
||||
|
||||
<hr class="separator--horizontal margin-inline" style="--border-color: var(--color-subtle-dark);" aria-hidden="true" />
|
||||
|
||||
<% if User.active.count > 20 %>
|
||||
<div class="pad-inline">
|
||||
<input placeholder="Filter…" class="input input--transparent full-width" type="search" autocorrect="off" autocomplete="off" data-1p-ignore="true" data-action="input->filter#filter">
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<div data-filter-target="list" class="users-list pad-inline">
|
||||
<% 20.times do %>
|
||||
<%= access_toggles_for selected_users, selected: true %>
|
||||
<% end %>
|
||||
|
||||
<% if selected_users.any? && unselected_users.any? %>
|
||||
<hr class="separator full-width" style="--border-style: solid">
|
||||
<% end %>
|
||||
|
||||
<%= access_toggles_for unselected_users, selected: false %>
|
||||
</div>
|
||||
<% end %>
|
||||
</section>
|
||||
@@ -0,0 +1,21 @@
|
||||
<strong class="txt-large">Workflows</strong>
|
||||
<div class="flex justify-center margin-block-end">Use a Workflow to track progress in this Collection.</div>
|
||||
<div class="flex flex-wrap gap">
|
||||
<% Workflow.all.each do |workflow| %>
|
||||
<div class="workflow-preview txt-align-start border border-radius <%= 'workflow-preview--selected' if workflow == collection.workflow %>">
|
||||
<%= button_to collection_workflow_path(collection), method: :patch do %>
|
||||
<%= hidden_field_tag "collection[workflow_id]", workflow.id %>
|
||||
<strong class="txt-small overflow-ellipsis"><%= workflow.name %></strong>
|
||||
<ul class="list-style-none txt-xx-small ">
|
||||
<% workflow.stages.each do |stage| %>
|
||||
<li class="overflow-ellipsis flex align-center gap-half min-width">
|
||||
<span class="workflow-preview__swatch btn btn--circle" style="--btn-background: <%= stage.color.present? ? stage.color : Card::Colored::DEFAULT_COLOR %>;"></span>
|
||||
<span class="overflow-ellipsis"><%= stage.name %></span>
|
||||
</li>
|
||||
<% end %>
|
||||
</ul>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<p><%= link_to "Manage workflows", workflows_path, class: "btn btn--plain txt-link" %></p>
|
||||
@@ -0,0 +1,11 @@
|
||||
class AddAutoClosePeriodToCollections < ActiveRecord::Migration[8.1]
|
||||
def change
|
||||
add_column :collections, :auto_close_period, :bigint
|
||||
|
||||
add_index :collections, :auto_close_period
|
||||
|
||||
execute <<~SQL
|
||||
UPDATE collections SET auto_close_period = #{30.days.to_i} WHERE auto_close_period IS NULL;
|
||||
SQL
|
||||
end
|
||||
end
|
||||
Generated
+3
-2
@@ -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_04_25_070025) do
|
||||
ActiveRecord::Schema[8.1].define(version: 2025_04_25_092727) do
|
||||
create_table "accesses", force: :cascade do |t|
|
||||
t.integer "collection_id", null: false
|
||||
t.datetime "created_at", null: false
|
||||
@@ -141,11 +141,13 @@ ActiveRecord::Schema[8.1].define(version: 2025_04_25_070025) 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
|
||||
@@ -292,7 +294,6 @@ ActiveRecord::Schema[8.1].define(version: 2025_04_25_070025) do
|
||||
t.datetime "created_at", null: false
|
||||
t.string "title"
|
||||
t.datetime "updated_at", null: false
|
||||
t.index ["title"], name: "index_tags_on_account_id_and_title", unique: true
|
||||
end
|
||||
|
||||
create_table "users", force: :cascade do |t|
|
||||
|
||||
+28
-18
@@ -504,6 +504,16 @@ 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
|
||||
- *23
|
||||
- *6
|
||||
@@ -1401,6 +1411,22 @@ 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
|
||||
@@ -1941,23 +1967,7 @@ indexes:
|
||||
nulls_not_distinct:
|
||||
comment:
|
||||
valid: true
|
||||
tags:
|
||||
- !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition
|
||||
table: tags
|
||||
name: index_tags_on_account_id_and_title
|
||||
unique: true
|
||||
columns:
|
||||
- title
|
||||
lengths: {}
|
||||
orders: {}
|
||||
opclasses: {}
|
||||
where:
|
||||
type:
|
||||
using:
|
||||
include:
|
||||
nulls_not_distinct:
|
||||
comment:
|
||||
valid: true
|
||||
tags: []
|
||||
users:
|
||||
- !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition
|
||||
table: users
|
||||
@@ -2042,4 +2052,4 @@ indexes:
|
||||
comment:
|
||||
valid: true
|
||||
workflows: []
|
||||
version: 20250425070025
|
||||
version: 20250425092727
|
||||
|
||||
@@ -35,7 +35,7 @@ class CollectionsControllerTest < ActionDispatch::IntegrationTest
|
||||
user_ids: users(:david, :jz).pluck(:id)
|
||||
}
|
||||
|
||||
assert_redirected_to cards_path(collection_ids: [ collections(:writebook) ])
|
||||
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_not collections(:writebook).all_access?
|
||||
@@ -49,7 +49,7 @@ class CollectionsControllerTest < ActionDispatch::IntegrationTest
|
||||
|
||||
patch collection_path(collection), params: { collection: { name: "Bugs", all_access: true } }
|
||||
|
||||
assert_redirected_to cards_path(collection_ids: [ collection ])
|
||||
assert_redirected_to edit_collection_path(collection)
|
||||
assert collection.reload.all_access?
|
||||
assert_equal User.all, collection.users
|
||||
end
|
||||
|
||||
Vendored
+1
@@ -2,4 +2,5 @@ writebook:
|
||||
name: Writebook
|
||||
creator: david
|
||||
all_access: true
|
||||
auto_close_period: 30
|
||||
workflow: qa
|
||||
|
||||
@@ -15,11 +15,19 @@ class Card::CloseableTest < ActiveSupport::TestCase
|
||||
assert_equal users(:kevin), cards(:logo).closed_by
|
||||
end
|
||||
|
||||
test "auto_close_all_due" do
|
||||
test "autoclose_at infers the period from the collection" do
|
||||
freeze_time
|
||||
|
||||
collections(:writebook).update! auto_close_period: 123.days
|
||||
cards(:logo).update! last_active_at: 2.day.ago
|
||||
assert_equal (123-2).days.from_now, cards(:logo).auto_close_at
|
||||
end
|
||||
|
||||
test "auto close all due" do
|
||||
cards(:logo, :shipping).each(&:reconsider)
|
||||
|
||||
cards(:logo).update!(last_active_at: 1.day.ago - Card::Closeable::AUTO_CLOSE_AFTER)
|
||||
cards(:shipping).update!(last_active_at: 1.day.from_now - Card::Closeable::AUTO_CLOSE_AFTER)
|
||||
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
|
||||
@@ -28,4 +36,16 @@ class Card::CloseableTest < ActiveSupport::TestCase
|
||||
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
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user