Merge pull request #1174 from basecamp/collapsing-columns+domain
Collapsing columns+domain
This commit is contained in:
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(git merge:*)",
|
||||
"Bash(git add:*)"
|
||||
],
|
||||
"deny": [],
|
||||
"ask": []
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -608,7 +608,7 @@ GEM
|
||||
tiktoken_ruby (0.0.12-x86_64-linux)
|
||||
timeout (0.4.3)
|
||||
tsort (0.2.0)
|
||||
turbo-rails (2.0.16)
|
||||
turbo-rails (2.0.17)
|
||||
actionpack (>= 7.1.0)
|
||||
railties (>= 7.1.0)
|
||||
tzinfo (2.0.6)
|
||||
|
||||
@@ -7,6 +7,6 @@ class Account::EntropyConfigurationsController < ApplicationController
|
||||
|
||||
private
|
||||
def entropy_configuration_params
|
||||
params.expect(entropy_configuration: [ :auto_close_period, :auto_reconsider_period ])
|
||||
params.expect(entropy_configuration: [ :auto_postpone_period ])
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
class Cards::DropsController < ApplicationController
|
||||
include FilterScoped
|
||||
|
||||
before_action :set_card, :set_drop_target
|
||||
|
||||
def create
|
||||
perform_drop_action
|
||||
render_column_replacement
|
||||
end
|
||||
|
||||
private
|
||||
VALID_DROP_TARGETS = %w[ considering on_deck doing closed ]
|
||||
|
||||
def set_card
|
||||
@card = Current.user.accessible_cards.find(params[:dropped_item_id])
|
||||
end
|
||||
|
||||
def set_drop_target
|
||||
if params[:drop_target].in?(VALID_DROP_TARGETS)
|
||||
@drop_target = params[:drop_target].to_sym
|
||||
else
|
||||
head :bad_request
|
||||
end
|
||||
end
|
||||
|
||||
def perform_drop_action
|
||||
case @drop_target
|
||||
when :considering
|
||||
@card.reconsider
|
||||
when :on_deck
|
||||
@card.move_to_on_deck
|
||||
when :doing
|
||||
@card.engage
|
||||
if params[:stage_id].present?
|
||||
stage = Workflow::Stage.find(params[:stage_id])
|
||||
@card.change_stage_to(stage)
|
||||
end
|
||||
when :closed
|
||||
@card.close
|
||||
end
|
||||
end
|
||||
|
||||
def render_column_replacement
|
||||
columns = Cards::Columns.new(user_filtering: @user_filtering, page_size: CardsController::PAGE_SIZE)
|
||||
column = columns.public_send(@drop_target)
|
||||
|
||||
if @drop_target == :doing && params[:stage_id].present?
|
||||
# For stage-specific doing columns, we need to render the specific stage
|
||||
stage = Workflow::Stage.find(params[:stage_id])
|
||||
render \
|
||||
turbo_stream: turbo_stream.replace("doing-cards-#{stage.id}",
|
||||
method: :morph,
|
||||
partial: "cards/index/engagement/doing",
|
||||
locals: { column: column, stage: stage })
|
||||
else
|
||||
# For other columns, use the standard approach
|
||||
render \
|
||||
turbo_stream: turbo_stream.replace("#{@drop_target.to_s.gsub('_', '-')}-cards",
|
||||
method: :morph,
|
||||
partial: "cards/index/engagement/#{@drop_target}",
|
||||
locals: { column: column })
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,8 @@
|
||||
class Cards::NotNowsController < ApplicationController
|
||||
include CardScoped
|
||||
|
||||
def create
|
||||
@card.postpone
|
||||
render_card_replacement
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,15 @@
|
||||
class Cards::TriagesController < ApplicationController
|
||||
include CardScoped
|
||||
|
||||
def create
|
||||
column = @card.collection.columns.find(params[:column_id])
|
||||
@card.triage_into(column)
|
||||
|
||||
render_card_replacement
|
||||
end
|
||||
|
||||
def destroy
|
||||
@card.send_back_to_triage
|
||||
render_card_replacement
|
||||
end
|
||||
end
|
||||
@@ -11,8 +11,7 @@ class CardsController < ApplicationController
|
||||
PAGE_SIZE = 25
|
||||
|
||||
def index
|
||||
@columns = Cards::Columns.new(user_filtering: @user_filtering, page_size: PAGE_SIZE)
|
||||
|
||||
set_page_and_extract_portion_from @filter.cards
|
||||
fresh_when etag: @columns
|
||||
end
|
||||
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
class Collections::Columns::ClosedsController < ApplicationController
|
||||
include CollectionScoped
|
||||
|
||||
def show
|
||||
set_page_and_extract_portion_from @collection.cards.closed.recently_closed_first
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,7 @@
|
||||
class Collections::Columns::NotNowsController < ApplicationController
|
||||
include CollectionScoped
|
||||
|
||||
def show
|
||||
set_page_and_extract_portion_from @collection.cards.postponed.reverse_chronologically
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,7 @@
|
||||
class Collections::Columns::StreamsController < ApplicationController
|
||||
include CollectionScoped
|
||||
|
||||
def show
|
||||
set_page_and_extract_portion_from @collection.cards.awaiting_triage.reverse_chronologically
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,14 @@
|
||||
class Collections::ColumnsController < ApplicationController
|
||||
include CollectionScoped
|
||||
|
||||
before_action :set_column
|
||||
|
||||
def show
|
||||
set_page_and_extract_portion_from @column.cards.active.reverse_chronologically
|
||||
end
|
||||
|
||||
private
|
||||
def set_column
|
||||
@column = @collection.columns.find(params[:id])
|
||||
end
|
||||
end
|
||||
@@ -12,6 +12,6 @@ class Collections::EntropyConfigurationsController < ApplicationController
|
||||
|
||||
private
|
||||
def entropy_configuration_params
|
||||
params.expect(collection: [ :auto_close_period, :auto_reconsider_period ])
|
||||
params.expect(collection: [ :auto_postpone_period ])
|
||||
end
|
||||
end
|
||||
|
||||
@@ -7,6 +7,10 @@ class CollectionsController < ApplicationController
|
||||
@collection = Collection.new
|
||||
end
|
||||
|
||||
def show
|
||||
set_page_and_extract_portion_from @collection.cards.awaiting_triage.reverse_chronologically
|
||||
end
|
||||
|
||||
def create
|
||||
@collection = Collection.create! collection_params.with_defaults(all_access: true)
|
||||
redirect_to cards_path(collection_ids: [ @collection ])
|
||||
@@ -38,7 +42,7 @@ class CollectionsController < ApplicationController
|
||||
end
|
||||
|
||||
def collection_params
|
||||
params.expect(collection: [ :name, :all_access, :auto_close_period, :auto_reconsider_period, :public_description ])
|
||||
params.expect(collection: [ :name, :all_access, :auto_postpone_period, :public_description ])
|
||||
end
|
||||
|
||||
def grantees
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
class Columns::Cards::Drops::ClosuresController < ApplicationController
|
||||
include CardScoped
|
||||
|
||||
def create
|
||||
@card.close
|
||||
|
||||
render turbo_stream: turbo_stream.replace("closed-cards", partial: "collections/show/closed", method: :morph, locals: { collection: @card.collection })
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,10 @@
|
||||
class Columns::Cards::Drops::ColumnsController < ApplicationController
|
||||
include ActionView::RecordIdentifier, 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 })
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,9 @@
|
||||
class Columns::Cards::Drops::NotNowsController < ApplicationController
|
||||
include CardScoped
|
||||
|
||||
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
|
||||
@@ -0,0 +1,10 @@
|
||||
class Columns::Cards::Drops::StreamsController < ApplicationController
|
||||
include CardScoped
|
||||
|
||||
def create
|
||||
@card.send_back_to_triage
|
||||
set_page_and_extract_portion_from @collection.cards.awaiting_triage.reverse_chronologically
|
||||
|
||||
render turbo_stream: turbo_stream.replace("the-stream", partial: "collections/show/stream", method: :morph, locals: { collection: @card.collection, page: @page })
|
||||
end
|
||||
end
|
||||
@@ -6,10 +6,10 @@ class Public::CollectionsController < ApplicationController
|
||||
layout "public"
|
||||
|
||||
def show
|
||||
@considering = current_page_from @collection.cards.considering.latest, per_page: CardsController::PAGE_SIZE
|
||||
@on_deck = current_page_from @collection.cards.on_deck.latest, per_page: CardsController::PAGE_SIZE
|
||||
@doing = current_page_from @collection.cards.doing.latest, per_page: CardsController::PAGE_SIZE
|
||||
@closed = current_page_from @collection.cards.closed.recently_closed_first, per_page: CardsController::PAGE_SIZE
|
||||
# @considering = current_page_from @collection.cards.considering.latest, per_page: CardsController::PAGE_SIZE
|
||||
# @on_deck = current_page_from @collection.cards.on_deck.latest, per_page: CardsController::PAGE_SIZE
|
||||
# @doing = current_page_from @collection.cards.doing.latest, per_page: CardsController::PAGE_SIZE
|
||||
# @closed = current_page_from @collection.cards.closed.recently_closed_first, per_page: CardsController::PAGE_SIZE
|
||||
|
||||
# To enable caching at intermediate proxies during traffic spikes
|
||||
expires_in 5.seconds, public: true
|
||||
|
||||
@@ -35,8 +35,8 @@ module CardsHelper
|
||||
classes = [
|
||||
options.delete(:class),
|
||||
("golden-effect" if card.golden?),
|
||||
("card--considering" if card.considering?),
|
||||
("card--doing" if card.doing?),
|
||||
("card--postponed" if card.postponed?),
|
||||
("card--active" if card.active?),
|
||||
("card--drafted" if card.drafted?)
|
||||
].compact.join(" ")
|
||||
|
||||
@@ -71,8 +71,8 @@ module CardsHelper
|
||||
end
|
||||
|
||||
def cards_expander(title, count)
|
||||
tag.header class: "cards__expander", data: { action: "click->collapsible-columns#toggle" }, style: "--card-count: #{[ count, 20 ].min}", aria: { role: "button" } do
|
||||
concat(tag.span count > 99 ? "99+" : count, class: "cards__expander-count")
|
||||
tag.header class: "cards__expander", data: { action: "click->collapsible-columns#toggle" }, style: "--card-count: #{[count, 20].min}", aria: { role: "button" } do
|
||||
concat(tag.span count > 99 ? "99+" : count, class: "cards__expander-count", "data-drag-and-drop-counter": true)
|
||||
concat(tag.h2 title, class: "cards__expander-title")
|
||||
concat(tag.div class: "cards__expander-menu position-relative", data: { controller: "dialog", action: "keydown.esc->dialog#close click@document->dialog#closeOnClickOutside" } do
|
||||
concat(tag.button class: "btn btn--circle txt-x-small borderless", data: { action: "click->dialog#open:stop" } do
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
module ColumnsHelper
|
||||
def button_to_set_column(card, column)
|
||||
button_to \
|
||||
tag.span(column.name, class: "overflow-ellipsis"),
|
||||
card_triage_path(card, column_id: column),
|
||||
method: :post,
|
||||
class: [ "btn justify-start workflow-stage txt-uppercase", { "workflow-stage--current": column == card.column } ],
|
||||
form_class: "flex align-center gap-half",
|
||||
data: { turbo_frame: "_top" }
|
||||
end
|
||||
|
||||
def column_frame_tag(id, src: nil, data: {}, **options, &block)
|
||||
data = data.reverse_merge \
|
||||
"drag-and-drop-refresh": true,
|
||||
controller: "frame",
|
||||
action: "turbo:before-frame-render->frame#morphRender turbo:before-morph-element->frame#morphReload"
|
||||
|
||||
turbo_frame_tag(id, src: src, data: data, **options, &block)
|
||||
end
|
||||
end
|
||||
@@ -7,7 +7,7 @@ module EntropyHelper
|
||||
{
|
||||
daysBeforeReminder: card.entropy.days_before_reminder,
|
||||
closesAt: card.entropy.auto_clean_at.iso8601,
|
||||
action: card_entropy_action(card)
|
||||
action: "Postpones"
|
||||
}
|
||||
end
|
||||
|
||||
@@ -20,12 +20,4 @@ module EntropyHelper
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
def card_entropy_action(card)
|
||||
if card.stagnated?
|
||||
"Falls Back"
|
||||
elsif card.considering?
|
||||
"Closes"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -6,4 +6,3 @@ import "controllers"
|
||||
import "lexxy"
|
||||
import "@rails/actiontext"
|
||||
|
||||
window.fetch = Turbo.fetch // TODO: We need to make @rails/request.js use Turbo's fetch when it's present.
|
||||
|
||||
@@ -19,4 +19,10 @@ export default class extends Controller {
|
||||
clickedColumn.classList.remove(this.collapsedClass)
|
||||
}
|
||||
}
|
||||
|
||||
preventToggle(event) {
|
||||
if (event.detail.attributeName === "class") {
|
||||
event.preventDefault()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import { nextFrame } from "helpers/timing_helpers"
|
||||
|
||||
export default class extends Controller {
|
||||
static targets = [ "item", "container" ]
|
||||
static values = { url: String }
|
||||
static classes = [ "draggedItem", "hoverContainer" ]
|
||||
|
||||
// Actions
|
||||
@@ -38,8 +37,10 @@ export default class extends Controller {
|
||||
if (!container || container === this.sourceContainer) { return }
|
||||
|
||||
this.wasDropped = true
|
||||
|
||||
this.#decreaseCounter(this.sourceContainer)
|
||||
const sourceContainer = this.sourceContainer
|
||||
await this.#submitDropRequest(this.dragItem, container)
|
||||
this.#reloadSourceFrame(sourceContainer);
|
||||
}
|
||||
|
||||
dragEnd() {
|
||||
@@ -67,19 +68,30 @@ export default class extends Controller {
|
||||
this.containerTargets.forEach(container => container.classList.remove(this.hoverContainerClass))
|
||||
}
|
||||
|
||||
// Private
|
||||
|
||||
async #submitDropRequest(item, container) {
|
||||
const body = new FormData()
|
||||
const id = item.dataset.id
|
||||
const containerTarget = container.dataset.dropTarget
|
||||
const stageId = container.dataset.stageId
|
||||
const url = container.dataset.dragAndDropUrl.replaceAll("__id__", id)
|
||||
|
||||
body.append("dropped_item_id", id)
|
||||
body.append("drop_target", containerTarget)
|
||||
if (stageId) {
|
||||
body.append("stage_id", stageId)
|
||||
return post(url, { body, headers: { Accept: "text/vnd.turbo-stream.html" } })
|
||||
}
|
||||
|
||||
#reloadSourceFrame(sourceContainer) {
|
||||
const frame = sourceContainer.querySelector("[data-drag-and-drop-refresh]")
|
||||
if (frame) frame.reload()
|
||||
}
|
||||
|
||||
#decreaseCounter(sourceContainer) {
|
||||
const counterElement = sourceContainer.querySelector("[data-drag-and-drop-counter]")
|
||||
if (counterElement) {
|
||||
const currentValue = counterElement.textContent.trim()
|
||||
|
||||
if (!/^\d+$/.test(currentValue)) return
|
||||
|
||||
const count = parseInt(currentValue)
|
||||
if (count > 0) {
|
||||
counterElement.textContent = count - 1
|
||||
}
|
||||
}
|
||||
return post(this.urlValue, { body, headers: { Accept: "text/vnd.turbo-stream.html" } })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import { post } from "@rails/request.js"
|
||||
export default class extends Controller {
|
||||
static classes = ["filtersSet"]
|
||||
static targets = ["field", "form"]
|
||||
static values = { refreshUrl: String }
|
||||
static values = { refreshUrl: String, noFilteringUrl: String }
|
||||
|
||||
initialize() {
|
||||
this.debouncedToggle = debounce(this.#toggle.bind(this), 50)
|
||||
@@ -15,11 +15,26 @@ export default class extends Controller {
|
||||
this.#toggle()
|
||||
}
|
||||
|
||||
change() {
|
||||
change(event) {
|
||||
this.#toggle()
|
||||
this.#refreshSaveToggleButton()
|
||||
}
|
||||
|
||||
resetIfBlankAndNoFiltering(event) {
|
||||
const { target } = event
|
||||
if (!target.value.trim() && !this.#hasFiltersSet) {
|
||||
this.#showNoFilteringUrl();
|
||||
event.stopImmediatePropagation()
|
||||
}
|
||||
}
|
||||
|
||||
resetIfNoFiltering(event) {
|
||||
if (!this.#hasFiltersSet) {
|
||||
this.#showNoFilteringUrl()
|
||||
event.stopImmediatePropagation()
|
||||
}
|
||||
}
|
||||
|
||||
async fieldTargetConnected(field) {
|
||||
this.debouncedToggle()
|
||||
}
|
||||
@@ -65,4 +80,8 @@ export default class extends Controller {
|
||||
|
||||
return formData
|
||||
}
|
||||
|
||||
#showNoFilteringUrl() {
|
||||
Turbo.visit(this.noFilteringUrlValue, { frame: "cards_container", action: "advance" })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import { Controller } from "@hotwired/stimulus"
|
||||
import { Turbo } from "@hotwired/turbo-rails"
|
||||
|
||||
export default class extends Controller {
|
||||
morphRender({ detail }) {
|
||||
detail.render = function (currentElement, newElement) {
|
||||
Turbo.morphChildren(currentElement, newElement)
|
||||
}
|
||||
}
|
||||
|
||||
morphReload(event) {
|
||||
const newElement = event.detail.newElement
|
||||
if (newElement && newElement.tagName === "TURBO-FRAME") {
|
||||
event.preventDefault()
|
||||
this.element.reload()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
class Card::AutoCloseAllDueJob < ApplicationJob
|
||||
def perform
|
||||
ApplicationRecord.with_each_tenant do |tenant|
|
||||
Card.auto_close_all_due
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,7 @@
|
||||
class Card::AutoPostponeAllDueJob < ApplicationJob
|
||||
def perform
|
||||
ApplicationRecord.with_each_tenant do |tenant|
|
||||
Card.auto_postpone_all_due
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -12,7 +12,6 @@ module Account::Entropic
|
||||
|
||||
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
|
||||
auto_postpone_period: DEFAULT_ENTROPY_PERIOD
|
||||
end
|
||||
end
|
||||
|
||||
+4
-4
@@ -1,7 +1,7 @@
|
||||
class Card < ApplicationRecord
|
||||
include Assignable, Attachments, Cacheable, Closeable, Colored, Engageable, Entropic, Eventable,
|
||||
Golden, Mentions, Multistep, Pinnable, Promptable, Readable, Searchable,
|
||||
Staged, Stallable, Statuses, Taggable, Watchable
|
||||
include Assignable, Attachments, Cacheable, Closeable, Colored, Entropic, Eventable,
|
||||
Golden, Mentions, Multistep, Pinnable, Postponable, Promptable, Readable, Searchable,
|
||||
Staged, Stallable, Statuses, Taggable, Triageable, Watchable
|
||||
|
||||
belongs_to :collection, touch: true
|
||||
belongs_to :creator, class_name: "User", default: -> { Current.user }
|
||||
@@ -21,7 +21,7 @@ class Card < ApplicationRecord
|
||||
scope :indexed_by, ->(index) do
|
||||
case index
|
||||
when "stalled" then stalled
|
||||
when "closing_soon" then closing_soon
|
||||
when "postponing_soon" then postponing_soon
|
||||
when "falling_back_soon" then falling_back_soon
|
||||
when "closed" then closed.recently_closed_first
|
||||
when "golden" then golden
|
||||
|
||||
@@ -14,11 +14,11 @@ module Card::Colored
|
||||
DEFAULT_COLOR = "var(--color-card-default)"
|
||||
|
||||
def color
|
||||
color_from_stage || DEFAULT_COLOR
|
||||
color_from_column || DEFAULT_COLOR
|
||||
end
|
||||
|
||||
private
|
||||
def color_from_stage
|
||||
stage&.color&.presence if doing?
|
||||
def color_from_column
|
||||
column&.color&.presence
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
module Card::Engageable
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
has_one :engagement, dependent: :destroy, class_name: "Card::Engagement"
|
||||
|
||||
scope :considering, -> { published_or_drafted_by(Current.user).open.where.missing(:engagement) }
|
||||
scope :on_deck, -> { published.open.joins(:engagement).where(engagement: { status: "on_deck" }) }
|
||||
scope :doing, -> { published.open.joins(:engagement).where(engagement: { status: "doing" }) }
|
||||
|
||||
scope :by_engagement_status, ->(status) do
|
||||
case status.to_s
|
||||
when "considering" then considering.with_golden_first
|
||||
when "on_deck" then on_deck.with_golden_first
|
||||
when "doing" then doing.with_golden_first
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def doing?
|
||||
open? && published? && engagement&.status == "doing"
|
||||
end
|
||||
|
||||
def on_deck?
|
||||
open? && published? && engagement&.status == "on_deck"
|
||||
end
|
||||
|
||||
def considering?
|
||||
open? && published? && engagement.blank?
|
||||
end
|
||||
|
||||
def engagement_status
|
||||
if doing?
|
||||
"doing"
|
||||
elsif on_deck?
|
||||
"on_deck"
|
||||
elsif considering?
|
||||
"considering"
|
||||
end
|
||||
end
|
||||
|
||||
def engage
|
||||
unless doing?
|
||||
reengage(status: "doing")
|
||||
end
|
||||
end
|
||||
|
||||
def move_to_on_deck
|
||||
unless on_deck?
|
||||
reengage(status: "on_deck")
|
||||
end
|
||||
end
|
||||
|
||||
def reconsider
|
||||
transaction do
|
||||
reopen
|
||||
engagement&.destroy
|
||||
activity_spike&.destroy
|
||||
update!(stage: nil)
|
||||
touch_last_active_at
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def reengage(status:)
|
||||
transaction do
|
||||
reopen
|
||||
engagement&.destroy
|
||||
create_engagement!(status:)
|
||||
if status == "doing" && stage.blank?
|
||||
update!(stage: collection.initial_workflow_stage)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
+12
-30
@@ -2,43 +2,29 @@ module Card::Entropic
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
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.or(on_deck).entropic_by(:auto_reconsider_period) }
|
||||
|
||||
scope :due_to_be_closed, -> { considering.entropic_by(:auto_close_period) }
|
||||
|
||||
scope :closing_soon, -> do
|
||||
considering
|
||||
scope :due_to_be_postponed, -> do
|
||||
active
|
||||
.left_outer_joins(collection: :entropy_configuration)
|
||||
.where("last_active_at > DATETIME('now', '-' || COALESCE(entropy_configurations.auto_close_period, ?) || ' seconds')", Entropy::Configuration.default.auto_close_period)
|
||||
.where("last_active_at <= DATETIME('now', '-' || CAST(COALESCE(entropy_configurations.auto_close_period, ?) * 0.75 AS INTEGER) || ' seconds')", Entropy::Configuration.default.auto_close_period)
|
||||
.where("last_active_at <= DATETIME('now', '-' || COALESCE(entropy_configurations.auto_postpone_period, ?) || ' seconds')",
|
||||
Entropy::Configuration.default.auto_postpone_period)
|
||||
end
|
||||
|
||||
scope :falling_back_soon, -> do
|
||||
doing.or(on_deck)
|
||||
scope :postponing_soon, -> do
|
||||
active
|
||||
.left_outer_joins(collection: :entropy_configuration)
|
||||
.where("last_active_at > DATETIME('now', '-' || COALESCE(entropy_configurations.auto_reconsider_period, ?) || ' seconds')", Entropy::Configuration.default.auto_reconsider_period)
|
||||
.where("last_active_at <= DATETIME('now', '-' || CAST(COALESCE(entropy_configurations.auto_reconsider_period, ?) * 0.75 AS INTEGER) || ' seconds')", Entropy::Configuration.default.auto_reconsider_period)
|
||||
.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)
|
||||
end
|
||||
|
||||
delegate :auto_close_period, :auto_reconsider_period, to: :collection
|
||||
delegate :auto_postpone_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")
|
||||
def auto_postpone_all_due
|
||||
due_to_be_postponed.find_each do |card|
|
||||
card.postpone
|
||||
end
|
||||
end
|
||||
|
||||
def auto_reconsider_all_stagnated
|
||||
stagnated.find_each(&:reconsider)
|
||||
end
|
||||
end
|
||||
|
||||
def entropy
|
||||
@@ -48,8 +34,4 @@ module Card::Entropic
|
||||
def entropic?
|
||||
entropy.present?
|
||||
end
|
||||
|
||||
def stagnated?
|
||||
card.doing? || card.on_deck?
|
||||
end
|
||||
end
|
||||
|
||||
@@ -5,11 +5,7 @@ class Card::Entropy
|
||||
def for(card)
|
||||
return unless card.last_active_at
|
||||
|
||||
if card.considering?
|
||||
new(card, card.auto_close_period)
|
||||
elsif card.stagnated?
|
||||
new(card, card.auto_reconsider_period)
|
||||
end
|
||||
new(card, card.auto_postpone_period)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
class Card::NotNow < ApplicationRecord
|
||||
belongs_to :card, class_name: "::Card", touch: true
|
||||
end
|
||||
@@ -0,0 +1,35 @@
|
||||
module Card::Postponable
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
has_one :not_now, dependent: :destroy, class_name: "Card::NotNow"
|
||||
|
||||
scope :postponed, -> { open.published.joins(:not_now) }
|
||||
scope :active, -> { open.published.where.missing(:not_now) }
|
||||
end
|
||||
|
||||
def postponed?
|
||||
open? && published? && not_now.present?
|
||||
end
|
||||
|
||||
def active?
|
||||
open? && published? && !postponed?
|
||||
end
|
||||
|
||||
def postpone
|
||||
transaction do
|
||||
send_back_to_triage
|
||||
reopen
|
||||
activity_spike&.destroy
|
||||
create_not_now! unless postponed?
|
||||
end
|
||||
end
|
||||
|
||||
def resume
|
||||
transaction do
|
||||
reopen
|
||||
activity_spike&.destroy
|
||||
not_now&.destroy
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -7,7 +7,7 @@ module Card::Stallable
|
||||
has_one :activity_spike, class_name: "Card::ActivitySpike", dependent: :destroy
|
||||
|
||||
scope :with_activity_spikes, -> { joins(:activity_spike) }
|
||||
scope :stalled, -> { open.with_activity_spikes.where("card_activity_spikes.updated_at": ..STALLED_AFTER_LAST_SPIKE_PERIOD.ago) }
|
||||
scope :stalled, -> { open.active.with_activity_spikes.where("card_activity_spikes.updated_at": ..STALLED_AFTER_LAST_SPIKE_PERIOD.ago) }
|
||||
|
||||
before_update :remember_to_detect_activity_spikes
|
||||
after_update_commit :detect_activity_spikes_later, if: :should_detect_activity_spikes?
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
module Card::Triageable
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
belongs_to :column, optional: true
|
||||
|
||||
scope :awaiting_triage, -> { active.where.missing(:column) }
|
||||
scope :triaged, -> { active.joins(:column) }
|
||||
end
|
||||
|
||||
def triaged?
|
||||
active? && column.present?
|
||||
end
|
||||
|
||||
def awaiting_triage?
|
||||
active? && !triaged?
|
||||
end
|
||||
|
||||
def triage_into(column)
|
||||
raise "The column must belong to the card collection" unless collection == column.collection
|
||||
|
||||
transaction do
|
||||
resume
|
||||
activity_spike&.destroy
|
||||
update! column: column
|
||||
end
|
||||
end
|
||||
|
||||
def send_back_to_triage
|
||||
transaction do
|
||||
resume
|
||||
update!(column: nil)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,45 +0,0 @@
|
||||
class Cards::Columns
|
||||
attr_reader :user_filtering, :page_size
|
||||
|
||||
delegate :filter, to: :user_filtering
|
||||
|
||||
def initialize(user_filtering:, page_size:)
|
||||
@user_filtering = user_filtering
|
||||
@page_size = page_size
|
||||
end
|
||||
|
||||
def considering
|
||||
@considering ||= build_column_for "considering"
|
||||
end
|
||||
|
||||
def on_deck
|
||||
@on_deck ||= build_column_for "on_deck"
|
||||
end
|
||||
|
||||
def doing
|
||||
@doing ||= build_column_for "doing"
|
||||
end
|
||||
|
||||
def closed
|
||||
@closed ||= if filter.indexed_by.stalled?
|
||||
build_column(filter) { |cards| cards.recently_closed_first }
|
||||
else
|
||||
build_column(filter.with(indexed_by: "closed")) { |cards| cards.recently_closed_first }
|
||||
end
|
||||
end
|
||||
|
||||
def cache_key
|
||||
ActiveSupport::Cache.expand_cache_key([ considering, on_deck, doing, closed, Workflow.all, user_filtering ])
|
||||
end
|
||||
|
||||
private
|
||||
def build_column_for(engagement_status)
|
||||
build_column(filter.with(engagement_status: engagement_status))
|
||||
end
|
||||
|
||||
def build_column(filter, &block)
|
||||
cards = block ? yield(filter.cards) : filter.cards
|
||||
|
||||
Column.new(page: GearedPagination::Recordset.new(cards, per_page: page_size).page(1), filter: filter, user_filtering: user_filtering)
|
||||
end
|
||||
end
|
||||
@@ -1,17 +0,0 @@
|
||||
class Cards::Columns::Column
|
||||
attr_reader :page, :filter, :user_filtering
|
||||
|
||||
def initialize(page:, filter:, user_filtering:)
|
||||
@page = page
|
||||
@filter = filter
|
||||
@user_filtering = user_filtering
|
||||
end
|
||||
|
||||
def cards
|
||||
page.records
|
||||
end
|
||||
|
||||
def cache_key
|
||||
ActiveSupport::Cache.expand_cache_key([ cards ])
|
||||
end
|
||||
end
|
||||
@@ -1,5 +1,5 @@
|
||||
class Collection < ApplicationRecord
|
||||
include AutoClosing, Accessible, Broadcastable, Entropic, Filterable, Publishable, Workflowing
|
||||
include AutoClosing, Accessible, Broadcastable, Entropic, Filterable, Publishable, Triageable, Workflowing
|
||||
|
||||
belongs_to :creator, class_name: "User", default: -> { Current.user }
|
||||
|
||||
|
||||
@@ -2,13 +2,13 @@ module Collection::AutoClosing
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
before_create :set_default_auto_close_period
|
||||
before_create :set_default_auto_postpone_period
|
||||
end
|
||||
|
||||
private
|
||||
DEFAULT_AUTO_CLOSE_PERIOD = 30.days
|
||||
DEFAULT_auto_postpone_period = 30.days
|
||||
|
||||
def set_default_auto_close_period
|
||||
self.auto_close_period ||= DEFAULT_AUTO_CLOSE_PERIOD unless attribute_present?(:auto_close_period)
|
||||
def set_default_auto_postpone_period
|
||||
self.auto_postpone_period ||= DEFAULT_auto_postpone_period unless attribute_present?(:auto_postpone_period)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -2,20 +2,15 @@ module Collection::Entropic
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
delegate :auto_close_period, :auto_reconsider_period, to: :entropy_configuration
|
||||
delegate :auto_postpone_period, to: :entropy_configuration
|
||||
end
|
||||
|
||||
def entropy_configuration
|
||||
super || Entropy::Configuration.default
|
||||
end
|
||||
|
||||
def auto_close_period=(new_value)
|
||||
def auto_postpone_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
|
||||
entropy_configuration.update auto_postpone_period: new_value
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
module Collection::Triageable
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
has_many :columns, dependent: :destroy
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,7 @@
|
||||
class Column < ApplicationRecord
|
||||
belongs_to :collection
|
||||
has_many :cards, dependent: :nullify
|
||||
|
||||
validates :name, presence: true
|
||||
validates :color, presence: true
|
||||
end
|
||||
@@ -23,12 +23,11 @@ class Filter < ApplicationRecord
|
||||
result = result.sorted_by(sorted_by)
|
||||
result = result.where(id: card_ids) if card_ids.present?
|
||||
result = result.open unless include_closed_cards?
|
||||
result = result.by_engagement_status(engagement_status) if engagement_status.present?
|
||||
result = result.unassigned if assignment_status.unassigned?
|
||||
result = result.assigned_to(assignees.ids) if assignees.present?
|
||||
result = result.where(creator_id: creators.ids) if creators.present?
|
||||
result = result.where(collection: collections.ids) if collections.present?
|
||||
result = result.in_stage(stages.ids) if stages.present? && engagement_status&.doing?
|
||||
result = result.in_stage(stages.ids) if stages.present?
|
||||
result = result.tagged_with(tags.ids) if tags.present?
|
||||
result = result.where("cards.created_at": creation_window) if creation_window
|
||||
result = result.closed_at_window(closure_window) if closure_window
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
module Filter::Fields
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
INDEXES = %w[ all stalled closing_soon falling_back_soon golden draft ]
|
||||
INDEXES = %w[ all stalled postponing_soon falling_back_soon golden draft ]
|
||||
SORTED_BY = %w[ newest oldest latest ]
|
||||
|
||||
delegate :default_value?, to: :class
|
||||
@@ -18,7 +18,7 @@ module Filter::Fields
|
||||
|
||||
included do
|
||||
store_accessor :fields, :assignment_status, :indexed_by, :sorted_by, :terms,
|
||||
:engagement_status, :card_ids, :creation, :closure
|
||||
:card_ids, :creation, :closure
|
||||
|
||||
def assignment_status
|
||||
super.to_s.inquiry
|
||||
@@ -32,10 +32,6 @@ module Filter::Fields
|
||||
(super || default_sorted_by).inquiry
|
||||
end
|
||||
|
||||
def engagement_status
|
||||
super&.inquiry
|
||||
end
|
||||
|
||||
def creation_window
|
||||
TimeWindowParser.parse(creation)
|
||||
end
|
||||
|
||||
@@ -5,7 +5,6 @@ module Filter::Params
|
||||
:assignment_status,
|
||||
:indexed_by,
|
||||
:sorted_by,
|
||||
:engagement_status,
|
||||
:creation,
|
||||
:closure,
|
||||
card_ids: [],
|
||||
@@ -54,7 +53,6 @@ module Filter::Params
|
||||
@as_params ||= {}.tap do |params|
|
||||
params[:indexed_by] = indexed_by
|
||||
params[:sorted_by] = sorted_by
|
||||
params[:engagement_status] = engagement_status
|
||||
params[:creation] = creation
|
||||
params[:closure] = closure
|
||||
params[:assignment_status] = assignment_status
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
<%= render "cards/container/title", card: card %>
|
||||
<%= render "cards/display/perma/steps", card: card %>
|
||||
</div>
|
||||
<%= render "cards/stagings/stages", card: card if card.open? %>
|
||||
<%= render "cards/triage/columns", card: card if card.open? %>
|
||||
</div>
|
||||
|
||||
<footer class="card__footer full-width flex align-start gap">
|
||||
|
||||
@@ -8,9 +8,9 @@
|
||||
<%= render "cards/display/preview/steps", card: card %>
|
||||
<%= icon_tag "attachment", class: "card__attachments-indicator translucent" if card.has_attachments? %>
|
||||
|
||||
<% if card.staged? && card.doing? %>
|
||||
<% if card.triaged? %>
|
||||
<span class="btn justify-start workflow-stage workflow-stage--current txt-uppercase min-width max-width">
|
||||
<span class="overflow-ellipsis "><%= card.stage.name %></span>
|
||||
<span class="overflow-ellipsis "><%= card.column.name %></span>
|
||||
</span>
|
||||
<% end %>
|
||||
</header>
|
||||
@@ -22,7 +22,7 @@
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<%= render "cards/display/preview/stages", card: card if card.doing? %>
|
||||
<%= render "cards/display/preview/stages", card: card %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<%= tag.div hidden: true, class: "bubble bubble--#{card.engagement_status}",
|
||||
<%= tag.div hidden: true, class: "bubble",
|
||||
data: {
|
||||
controller: "bubble",
|
||||
action: "turbo:morph-element->bubble#update",
|
||||
|
||||
@@ -12,12 +12,6 @@
|
||||
<% content_for :header do %>
|
||||
<%= render "filters/menu", user_filtering: @user_filtering %>
|
||||
|
||||
<div class="header__actions header__actions--start">
|
||||
<% if collection = @filter.single_collection %>
|
||||
<%= render "cards/webhooks", collection: collection if Current.user.admin? %>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<h1 class="header__title divider divider--fade full-width">
|
||||
<span class="overflow-ellipsis"><%= @user_filtering.selected_collections_label %></span>
|
||||
</h1>
|
||||
@@ -29,19 +23,10 @@
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<%= render "filters/settings", user_filtering: @user_filtering %>
|
||||
<%= render "filters/settings", user_filtering: @user_filtering, no_filtering_url: cards_path %>
|
||||
|
||||
<%= tag.div data: {
|
||||
controller: "drag-and-drop drag-and-strum",
|
||||
drag_and_drop_dragged_item_class: "drag-and-drop__dragged-item",
|
||||
drag_and_drop_hover_container_class: "drag-and-drop__hover-container",
|
||||
drag_and_drop_url_value: cards_drops_path(**@filter.as_params),
|
||||
action: "
|
||||
dragstart->drag-and-drop#dragStart
|
||||
dragover->drag-and-drop#dragOver
|
||||
dragenter->drag-and-strum#dragEnter
|
||||
drop->drag-and-drop#drop
|
||||
dragend->drag-and-drop#dragEnd" } do %>
|
||||
|
||||
<%= render "cards/index/columns", columns: @columns %>
|
||||
<% end %>
|
||||
<section class="cards">
|
||||
<%= turbo_frame_tag :cards_container do %>
|
||||
<%= render partial: "cards/display/preview", collection: @page.records, as: :card, locals: { draggable: true }, cached: ->(card) { cacheable_preview_parts_for(card) } %>
|
||||
<% end %>
|
||||
</section>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<% cache columns do %>
|
||||
<%= turbo_frame_tag :card_columns do %>
|
||||
<%= turbo_frame_tag :cards_container do %>
|
||||
<div class="card-columns" data-controller="collapsible-columns" data-collapsible-columns-collapsed-class="is-collapsed">
|
||||
<div class="card-columns__left">
|
||||
<%= render "cards/index/engagement/on_deck", column: columns.on_deck %>
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<% content_for :header do %>
|
||||
<%= render "filters/menu", user_filtering: @user_filtering %>
|
||||
|
||||
<%= link_to cards_path(collection_ids: [ @card.collection ]), class: "header__title btn borderless txt-large", style: "--btn-padding: 0.25ch 1ch 0.25ch 0.75ch; view-transistion-name: card-collection-title;", data: { controller: "hotkey", action: "keydown.esc@document->hotkey#click" } do %>
|
||||
<%= link_to collection_path(@card.collection), class: "header__title btn borderless txt-large", style: "--btn-padding: 0.25ch 1ch 0.25ch 0.75ch; view-transistion-name: card-collection-title;", data: { controller: "hotkey", action: "keydown.esc@document->hotkey#click" } do %>
|
||||
<span class="overflow-ellipsis">
|
||||
←
|
||||
<strong class="font-black"><%= @card.collection.name %></strong>
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
<div id="<%= dom_id(card, :stages) %>" class="card__stages">
|
||||
<%= button_to "Not now", card_not_now_path(card),
|
||||
class: [ "btn justify-start workflow-stage txt-uppercase", { "workflow-stage--current": card.postponed? } ],
|
||||
form_class: "flex align-center gap-half" %>
|
||||
<%= button_to "The Stream", card_triage_path(card), method: :delete,
|
||||
class: [ "btn justify-start workflow-stage txt-uppercase", { "workflow-stage--current": card.awaiting_triage? } ],
|
||||
form_class: "flex align-center gap-half" %>
|
||||
<% card.collection.columns.each do |column| %>
|
||||
<%= button_to_set_column card, column %>
|
||||
<% end %>
|
||||
<%= button_to "Closed", card_closure_path(card, reason: "Completed"),
|
||||
class: [ "btn justify-start workflow-stage txt-uppercase", { "workflow-stage--current": card.closed? } ],
|
||||
form_class: "flex align-center gap-half" %>
|
||||
</div>
|
||||
@@ -0,0 +1 @@
|
||||
<%= render partial: "cards/display/preview", collection: cards, as: :card, locals: { draggable: draggable }, cached: ->(card) { card.cache_invalidation_parts.for_preview } %>
|
||||
@@ -0,0 +1,14 @@
|
||||
<%= turbo_frame_tag :closed_column do %>
|
||||
<% if @page.used? %>
|
||||
<%= render "collections/columns/list", cards: @page.records, draggable: true %>
|
||||
|
||||
<% unless @page.last? %>
|
||||
<div class="full-width">
|
||||
</div>
|
||||
<% end %>
|
||||
<% else %>
|
||||
<div class="card blank-slate blank-slate--card">
|
||||
<p class="txt-align-center txt-normal">Drag cards here</p>
|
||||
</div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
@@ -0,0 +1,15 @@
|
||||
<%= turbo_frame_tag :not_now_column do %>
|
||||
|
||||
<% if @page.used? %>
|
||||
<%= render "collections/columns/list", cards: @page.records, draggable: true %>
|
||||
|
||||
<% unless @page.last? %>
|
||||
<div class="full-width">
|
||||
</div>
|
||||
<% end %>
|
||||
<% else %>
|
||||
<div class="card blank-slate blank-slate--card">
|
||||
<p class="txt-align-center txt-normal">Drag cards here</p>
|
||||
</div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
@@ -0,0 +1,14 @@
|
||||
<%= turbo_frame_tag dom_id(@column, :cards) do %>
|
||||
<% if @page.used? %>
|
||||
<%= render "collections/columns/list", cards: @page.records, draggable: true %>
|
||||
|
||||
<% unless @page.last? %>
|
||||
<div class="full-width">
|
||||
</div>
|
||||
<% end %>
|
||||
<% else %>
|
||||
<div class="card blank-slate blank-slate--card">
|
||||
<p class="txt-align-center txt-normal">Drag cards here</p>
|
||||
</div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
@@ -0,0 +1,14 @@
|
||||
<%= turbo_frame_tag :stream_column do %>
|
||||
<% if @page.used? %>
|
||||
<%= render "collections/columns/list", cards: @page.records, draggable: true %>
|
||||
|
||||
<% unless @page.last? %>
|
||||
<div class="full-width">
|
||||
</div>
|
||||
<% end %>
|
||||
<% else %>
|
||||
<div class="card blank-slate blank-slate--card">
|
||||
<p class="txt-align-center txt-normal">Drag cards here</p>
|
||||
</div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
@@ -0,0 +1,39 @@
|
||||
<% @page_title = @collection.name %>
|
||||
<% turbo_exempts_page_from_cache %>
|
||||
|
||||
<% content_for :head do %>
|
||||
<%= tag.meta property: "og:title", content: @page_title %>
|
||||
<%= tag.meta property: "og:description", content: Account.sole.name %>
|
||||
<%= tag.meta property: "og:url", content: collection_url(@collection) %>
|
||||
<% end %>
|
||||
|
||||
<% content_for :header do %>
|
||||
<%= render "filters/menu", user_filtering: @user_filtering %>
|
||||
|
||||
<div class="header__actions header__actions--start">
|
||||
<%= render "cards/webhooks", collection: @collection if Current.user.admin? %>
|
||||
</div>
|
||||
|
||||
<h1 class="header__title divider divider--fade full-width">
|
||||
<span class="overflow-ellipsis"><%= @collection.name %></span>
|
||||
</h1>
|
||||
|
||||
<div class="header__actions header__actions--end">
|
||||
<%= render "cards/collection_settings", collection: @collection %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<%= render "filters/settings", user_filtering: @user_filtering, no_filtering_url: collection_path(@collection) %>
|
||||
|
||||
<%= tag.div data: {
|
||||
controller: "drag-and-drop",
|
||||
drag_and_drop_dragged_item_class: "drag-and-drop__dragged-item",
|
||||
drag_and_drop_hover_container_class: "drag-and-drop__hover-container",
|
||||
action: "
|
||||
dragstart->drag-and-drop#dragStart
|
||||
dragover->drag-and-drop#dragOver
|
||||
drop->drag-and-drop#drop
|
||||
dragend->drag-and-drop#dragEnd" } do %>
|
||||
|
||||
<%= render "collections/show/columns", page: @page, collection: @collection %>
|
||||
<% end %>
|
||||
@@ -0,0 +1,11 @@
|
||||
<section id="closed-cards" class="cards cards--on-deck is-collapsed" style="--card-color: var(--color-card-complete);"
|
||||
data-drag-and-drop-target="container"
|
||||
data-collapsible-columns-target="column"
|
||||
data-action="turbo:before-morph-attribute->collapsible-columns#preventToggle"
|
||||
data-drag-and-drop-url="<%= columns_card_drops_closure_path("__id__") %>"
|
||||
>
|
||||
|
||||
<%= cards_expander "Done", collection.cards.closed.count %>
|
||||
|
||||
<%= column_frame_tag :closed_column, src: collection_columns_closed_path(collection) %>
|
||||
</section>
|
||||
@@ -0,0 +1,11 @@
|
||||
<section id="<%= dom_id(column) %>"
|
||||
class="cards cards--doing is-collapsed" style="--card-color: <%= column.color %>;"
|
||||
data-drag-and-drop-target="container"
|
||||
data-collapsible-columns-target="column"
|
||||
data-action="turbo:before-morph-attribute->collapsible-columns#preventToggle"
|
||||
data-drag-and-drop-url="<%= columns_card_drops_column_path("__id__", column_id: column.id) %>"
|
||||
>
|
||||
<%= cards_expander column.name, column.cards.active.count %>
|
||||
|
||||
<%= column_frame_tag dom_id(column, :cards), src: collection_column_path(column.collection, column) %>
|
||||
</section>
|
||||
@@ -0,0 +1,20 @@
|
||||
<%= turbo_frame_tag :cards_container do %>
|
||||
<div class="card-columns" data-controller="collapsible-columns" data-collapsible-columns-collapsed-class="is-collapsed">
|
||||
|
||||
<div class="card-columns__left">
|
||||
<%= render "collections/show/not_now", collection: collection %>
|
||||
</div>
|
||||
|
||||
<%= render "collections/show/stream", collection: collection, page: page %>
|
||||
|
||||
<div class="card-columns__right">
|
||||
<%= render partial: "collections/show/column", collection: collection.columns, cached: true %>
|
||||
<%= render "collections/show/closed", collection: collection %>
|
||||
|
||||
<button class="btn btn--circle txt-x-small borderless">
|
||||
<%= icon_tag "menu-dots-horizontal" %>
|
||||
<span class="for-screen-reader">Add a new column, etc.</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
@@ -0,0 +1,11 @@
|
||||
<section id="not-now" class="cards cards--on-deck is-collapsed" style="--card-color: var(--color-card-complete);"
|
||||
data-collapsible-columns-target="column"
|
||||
data-drag-and-drop-target="container"
|
||||
data-action="turbo:before-morph-attribute->collapsible-columns#preventToggle"
|
||||
data-drag-and-drop-url="<%= columns_card_drops_not_now_path("__id__") %>"
|
||||
>
|
||||
|
||||
<%= cards_expander "Not Now", collection.cards.postponed.count %>
|
||||
|
||||
<%= column_frame_tag :not_now_column, src: collection_columns_not_now_path(collection) %>
|
||||
</section>
|
||||
@@ -0,0 +1,19 @@
|
||||
<section id="the-stream" class="cards cards--considering"
|
||||
data-drag-and-drop-target="container"
|
||||
data-action="turbo:before-morph-attribute->collapsible-columns#preventToggle"
|
||||
data-drag-and-drop-url="<%= columns_card_drops_stream_path("__id__") %>">
|
||||
<div class="cards__decoration"></div>
|
||||
|
||||
<%= render "columns/show/add_card_button", collection: collection %>
|
||||
|
||||
<% if page.used? %>
|
||||
<%= render "collections/columns/list", cards: page.records, draggable: true %>
|
||||
|
||||
<% unless page.last? %>
|
||||
<div class="full-width">
|
||||
</div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
<div class="cards__decoration cards__decoration--end"></div>
|
||||
</section>
|
||||
@@ -0,0 +1,13 @@
|
||||
<div class="card card--new flex flex-column gap-half">
|
||||
<%= button_to collection_cards_path(collection), method: :post, class: "btn btn--link", form: { data: { turbo_frame: "_top" } } do %>
|
||||
<%= icon_tag "add" %>
|
||||
<span>Add a card</span>
|
||||
<% end %>
|
||||
|
||||
<hr class="separator--horizontal full-width" aria-hidden="true">
|
||||
|
||||
<footer class="flex flex-column align-center gap-half">
|
||||
<strong class="txt-uppercase">Watching for new cards</strong>
|
||||
<%= access_involvement_advance_button(collection, Current.user, show_watchers: true) %>
|
||||
</footer>
|
||||
</div>
|
||||
@@ -7,7 +7,7 @@
|
||||
<%= form_with model: model, url: url, data: { controller: "form" } do |form| %>
|
||||
<%= render "entropy/knob",
|
||||
form: form,
|
||||
name: :auto_close_period,
|
||||
name: :auto_postpone_period,
|
||||
knob_options: entropy_auto_close_options,
|
||||
label: "Days until auto-close" %>
|
||||
<% end %>
|
||||
|
||||
@@ -1,14 +1,19 @@
|
||||
<% cache user_filtering do %>
|
||||
|
||||
<%= tag.aside id: "filter-settings",
|
||||
class: class_names("filters flex align-center gap-half justify-center center margin-block-end", { "filters--expanded": user_filtering.expanded? }),
|
||||
data: {
|
||||
controller: "toggle-class filter-settings",
|
||||
toggle_class_toggle_class: "filters--expanded",
|
||||
filter_settings_filters_set_class: "filters--has-filters-set",
|
||||
filter_settings_no_filtering_url_value: no_filtering_url,
|
||||
filter_settings_refresh_url_value: settings_refresh_path } do %>
|
||||
<%= form_with url: cards_path, method: :get, class: "", data: { controller: "form", turbo_frame: "card_columns", filter_settings_target: "form", turbo_action: "advance" } do |form| %>
|
||||
<%= hidden_field_tag :expand_all, true, disabled: !user_filtering.expanded?, data: { toggle_enable_target: "element" } %>
|
||||
<%= form_with url: cards_path, method: :get, class: "", data: {
|
||||
controller: "form",
|
||||
turbo_frame: "cards_container",
|
||||
filter_settings_target: "form",
|
||||
action: "turbo:submit-end->filter-settings#resetIfNoFiltering",
|
||||
turbo_action: "advance" } do |form| %>
|
||||
<%= hidden_field_tag :expand_all, true, disabled: !user_filtering.expanded?, data: { toggle_enable_target: "element"} %>
|
||||
<% user_filtering.filter.collections.each do |collection| %>
|
||||
<%= filter_hidden_field_tag "collection_ids[]", collection.id %>
|
||||
<% end %>
|
||||
@@ -20,7 +25,7 @@
|
||||
<%= render "filters/settings/toggle", user_filtering: user_filtering %>
|
||||
</div>
|
||||
<% end %>
|
||||
<%= render "filters/settings/manage", user_filtering: user_filtering %>
|
||||
<%= render "filters/settings/manage", user_filtering: user_filtering, no_filtering_url: no_filtering_url %>
|
||||
<% end %>
|
||||
|
||||
<% end %>
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<%= icon_tag "check", size: 18, class: "checked" %>
|
||||
</label>
|
||||
|
||||
<%= link_to cards_path(collection_ids: [ collection ]), class: "popup__btn btn" do %>
|
||||
<%= link_to collection_path(collection), class: "popup__btn btn" do %>
|
||||
<span class="overflow-ellipsis"><%= collection.name %></span>
|
||||
<% end %>
|
||||
</li>
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
<%= tag.li class: "popup__item", data: {
|
||||
filter_target: "item", navigable_list_target: "item", multi_selection_combobox_target: "item", multi_selection_combobox_value: "unassigned", multi_selection_combobox_label: "No one" },
|
||||
role: "checkbox", aria: { checked: filter.assignment_status.unassigned? } do %>
|
||||
<button type="button" class="btn popup__btn" data-action="dialog#close multi-selection-combobox#clear form#submit filter-settings#change">
|
||||
<button type="button" class="btn popup__btn" data-action="dialog#close multi-selection-combobox#clear filter-settings#change form#submit">
|
||||
<span class="overflow-ellipsis flex-item-grow">No one</span>
|
||||
<%= icon_tag "check", class: "checked flex-item-justify-end", "aria-hidden": true %>
|
||||
</button>
|
||||
@@ -37,7 +37,7 @@
|
||||
<%= tag.li class: "popup__item", data: {
|
||||
filter_target: "item", navigable_list_target: "item", multi_selection_combobox_target: "item", multi_selection_combobox_value: user.id, multi_selection_combobox_label: user.familiar_name },
|
||||
role: "checkbox", aria: { checked: filter.assignees.include?(user) } do %>
|
||||
<button type="button" class="btn popup__btn" data-action="dialog#close multi-selection-combobox#change form#submit filter-settings#change">
|
||||
<button type="button" class="btn popup__btn" data-action="dialog#close multi-selection-combobox#change filter-settings#change form#submit">
|
||||
<span class="overflow-ellipsis flex-item-grow"><%= user.name %></span>
|
||||
<%= icon_tag "check", class: "checked flex-item-justify-end", "aria-hidden": true %>
|
||||
</button>
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
<%= tag.li class: "popup__item", data: {
|
||||
filter_target: "item", navigable_list_target: "item", multi_selection_combobox_target: "item", multi_selection_combobox_value: user.id, multi_selection_combobox_label: user.familiar_name },
|
||||
role: "checkbox", aria: { checked: filter.closers.include?(user) } do %>
|
||||
<button type="button" class="btn popup__btn" data-action="dialog#close multi-selection-combobox#change form#submit filter-settings#change">
|
||||
<button type="button" class="btn popup__btn" data-action="dialog#close multi-selection-combobox#change filter-settings#change form#submit">
|
||||
<span class="overflow-ellipsis flex-item-grow"><%= user.name %></span>
|
||||
<%= icon_tag "check", class: "checked flex-item-justify-end", "aria-hidden": true %>
|
||||
</button>
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
<%= tag.li class: "popup__item", data: {
|
||||
filter_target: "item", navigable_list_target: "item", multi_selection_combobox_target: "item", multi_selection_combobox_value: user.id, multi_selection_combobox_label: user.familiar_name },
|
||||
role: "checkbox", aria: { checked: filter.creators.include?(user) } do %>
|
||||
<button type="button" class="btn popup__btn" data-action="dialog#close multi-selection-combobox#change form#submit filter-settings#change">
|
||||
<button type="button" class="btn popup__btn" data-action="dialog#close multi-selection-combobox#change filter-settings#change form#submit">
|
||||
<span class="overflow-ellipsis flex-item-grow"><%= user.name %></span>
|
||||
<%= icon_tag "check", class: "checked flex-item-justify-end", "aria-hidden": true %>
|
||||
</button>
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
data: { controller: "dialog combobox",
|
||||
action: "keydown.esc->dialog#close click@document->dialog#closeOnClickOutside",
|
||||
filter_show: user_filtering.show_indexed_by?,
|
||||
turbo_action: "advance",
|
||||
combobox_default_value_value: "all",
|
||||
combobox_default_label_value: "Status",
|
||||
combobox_with_default_class: "quick-filter--with-default" } do %>
|
||||
@@ -23,7 +22,7 @@
|
||||
<ul class="popup__list" data-filter-target="list" role="listbox">
|
||||
<% Filter::INDEXES.each do |index| %>
|
||||
<%= tag.li class: "popup__item", data: { navigable_list_target: "item", combobox_target: "item", combobox_value: index, combobox_label: index.humanize }, role: "checkbox", aria: { checked: filter.indexed_by == index } do %>
|
||||
<button type="button" class="btn popup__btn" data-action="dialog#close combobox#change form#submit filter-settings#change">
|
||||
<button type="button" class="btn popup__btn" data-action="dialog#close combobox#change filter-settings#change form#submit">
|
||||
<span class="overflow-ellipsis flex-item-grow"><%= index == "all" ? "Status" : index.humanize %></span>
|
||||
<%= icon_tag "check", class: "checked flex-item-justify-end", "aria-hidden": true %>
|
||||
</button>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<div class="filters__manage">
|
||||
<%= render "filters/filter_toggle", filter: filter %>
|
||||
|
||||
<%= link_to cards_path(filter.as_params.except(:assignee_ids, :assignment_status, :card_ids, :creator_ids, :closer_ids, :stage_ids, :tag_ids, :terms, :indexed_by, :sorted_by, :creation, :closure)), class: "btn btn--remove txt-x-small" do %>
|
||||
<%= link_to no_filtering_url, class: "btn btn--remove txt-x-small" do %>
|
||||
<%= icon_tag "close" %>
|
||||
<span class="for-screen-reader">Clear all</span>
|
||||
<% end %>
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
controller: "dialog combobox",
|
||||
action: "keydown.esc->dialog#close click@document->dialog#closeOnClickOutside",
|
||||
filter_show: user_filtering.show_sorted_by?,
|
||||
turbo_action: "advance",
|
||||
combobox_default_value_value: "latest",
|
||||
combobox_with_default_class: "quick-filter--with-default" } do %>
|
||||
<button type="button" class="btn input input--select flex-inline txt-x-small" data-action="click->dialog#toggle:stop">
|
||||
@@ -25,7 +24,7 @@
|
||||
<%= tag.li class: "popup__item", data: {
|
||||
navigable_list_target: "item", combobox_target: "item", combobox_value: sort, combobox_label: sorted_by_label(sort) },
|
||||
role: "checkbox", aria: { checked: filter.sorted_by == sort } do %>
|
||||
<button type="button" class="btn popup__btn" data-action="dialog#close combobox#change form#submit filter-settings#change">
|
||||
<button type="button" class="btn popup__btn" data-action="dialog#close combobox#change filter-settings#change form#submit">
|
||||
<span class="overflow-ellipsis flex-item-grow"><%= sorted_by_label(sort) %></span>
|
||||
<%= icon_tag "check", class: "checked flex-item-justify-end", "aria-hidden": true %>
|
||||
</button>
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
<%= content_tag(:li, class: "popup__item", data: {
|
||||
filter_target: "item", navigable_list_target: "item", multi_selection_combobox_target: "item", multi_selection_combobox_value: tag.id, multi_selection_combobox_label: tag.hashtag },
|
||||
role: "checkbox", aria: { checked: filter.tags.include?(tag) }) do %>
|
||||
<button type="button" class="btn popup__btn" data-action="dialog#close multi-selection-combobox#change form#submit filter-settings#change">
|
||||
<button type="button" class="btn popup__btn" data-action="dialog#close multi-selection-combobox#change filter-settings#change form#submit">
|
||||
<span class="overflow-ellipsis flex-item-grow"><%= tag.hashtag %></span>
|
||||
<%= icon_tag "check", class: "checked flex-item-justify-end", "aria-hidden": true %>
|
||||
</button>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
autofocus: false, autocomplete: :off, autocorrect: "off", data: {
|
||||
"1p-ignore": "true",
|
||||
filter_settings_target: "field",
|
||||
action: "input->form#debouncedSubmit keydown.enter->form#submitToTopTarget" } %>
|
||||
action: "input->filter-settings#resetIfBlankAndNoFiltering input->form#debouncedSubmit keydown.enter->form#submitToTopTarget" } %>
|
||||
|
||||
<% if filter.terms.present? %>
|
||||
<% filter.terms.each do |term| %>
|
||||
|
||||
@@ -10,11 +10,11 @@
|
||||
autocomplete: "off",
|
||||
aria: { activedescendant: "" },
|
||||
data: {
|
||||
"1p-ignore": "true",
|
||||
filter_target: "input",
|
||||
nav_section_expander_target: "input",
|
||||
navigable_list_target: "input",
|
||||
action: "input->filter#filter" } %>
|
||||
"1p-ignore": "true",
|
||||
filter_target: "input",
|
||||
nav_section_expander_target: "input",
|
||||
navigable_list_target: "input",
|
||||
action: "input->filter#filter" } %>
|
||||
<button class="fizzy-menu__close btn borderless txt-small" data-action="dialog#close">
|
||||
<%= icon_tag "close" %>
|
||||
<span class="for-screen-reader">Close menu</span>
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
|
||||
<div class="card__body justify-space-between">
|
||||
<%= render "public/cards/show/title", card: @card %>
|
||||
<%= render "cards/display/public_preview/stages", card: @card if @card.doing? %>
|
||||
<%= render "cards/display/public_preview/stages", card: @card %>
|
||||
</div>
|
||||
|
||||
<%= render "public/cards/show/steps", card: @card %>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
production: &production
|
||||
auto_close_all_due:
|
||||
class: Card::AutoCloseAllDueJob
|
||||
auto_postpone_all_due:
|
||||
class: Card::AutoPostponeAllDueJob
|
||||
schedule: every hour
|
||||
auto_reconsider_all_inactive:
|
||||
class: Card::AutoReconsiderAllStagnatedJob
|
||||
|
||||
+23
-2
@@ -17,6 +17,14 @@ Rails.application.routes.draw do
|
||||
resource :involvement
|
||||
resource :publication
|
||||
resource :entropy_configuration
|
||||
|
||||
namespace :columns do
|
||||
resource :not_now
|
||||
resource :stream
|
||||
resource :closed
|
||||
end
|
||||
|
||||
resources :columns
|
||||
end
|
||||
|
||||
resources :cards, only: %i[ create ]
|
||||
@@ -28,18 +36,31 @@ Rails.application.routes.draw do
|
||||
end
|
||||
end
|
||||
|
||||
namespace :columns do
|
||||
resources :cards do
|
||||
scope module: "cards" do
|
||||
namespace :drops do
|
||||
resource :not_now
|
||||
resource :stream
|
||||
resource :closure
|
||||
resource :column
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
namespace :cards do
|
||||
resources :previews
|
||||
resources :drops
|
||||
end
|
||||
|
||||
resources :cards, only: %i[ index show edit update destroy ] do
|
||||
scope module: :cards do
|
||||
resource :engagement
|
||||
resource :goldness
|
||||
resource :image
|
||||
resource :pin
|
||||
resource :closure
|
||||
resource :not_now
|
||||
resource :triage
|
||||
resource :publish
|
||||
resource :reading
|
||||
resource :recover
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
class CreateColumnsAndAddColumnIdToCards < ActiveRecord::Migration[8.1]
|
||||
def change
|
||||
create_table :columns do |t|
|
||||
t.string :name, null: false
|
||||
t.string :color, null: false
|
||||
t.references :collection, null: false, foreign_key: true
|
||||
t.timestamps
|
||||
end
|
||||
|
||||
add_reference :cards, :column, foreign_key: true, index: true
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,8 @@
|
||||
class CreateCardNotNows < ActiveRecord::Migration[8.1]
|
||||
def change
|
||||
create_table :card_not_nows do |t|
|
||||
t.references :card, null: false, foreign_key: true, index: { unique: true }
|
||||
t.timestamps
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,17 @@
|
||||
class AddAutoPostponePeriod < ActiveRecord::Migration[8.1]
|
||||
def change
|
||||
add_column :entropy_configurations, :auto_postpone_period, :bigint, default: 30.days.to_i, null: false
|
||||
add_index :entropy_configurations, [:container_type, :container_id, :auto_postpone_period]
|
||||
|
||||
execute <<-SQL
|
||||
UPDATE entropy_configurations
|
||||
SET auto_postpone_period = auto_close_period
|
||||
SQL
|
||||
|
||||
remove_index :entropy_configurations, name: "idx_on_container_type_container_id_auto_close_perio_74dc880875"
|
||||
remove_index :entropy_configurations, name: "idx_on_container_type_container_id_auto_reconsider__583aaddbea"
|
||||
|
||||
remove_column :entropy_configurations, :auto_close_period, :bigint
|
||||
remove_column :entropy_configurations, :auto_reconsider_period, :bigint
|
||||
end
|
||||
end
|
||||
Generated
+24
-5
@@ -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_09_17_064006) do
|
||||
ActiveRecord::Schema[8.1].define(version: 2025_09_24_124737) do
|
||||
create_table "accesses", force: :cascade do |t|
|
||||
t.datetime "accessed_at"
|
||||
t.integer "collection_id", null: false
|
||||
@@ -131,8 +131,16 @@ ActiveRecord::Schema[8.1].define(version: 2025_09_17_064006) do
|
||||
t.index ["card_id"], name: "index_card_goldnesses_on_card_id", unique: true
|
||||
end
|
||||
|
||||
create_table "card_not_nows", force: :cascade do |t|
|
||||
t.integer "card_id", null: false
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.index ["card_id"], name: "index_card_not_nows_on_card_id", unique: true
|
||||
end
|
||||
|
||||
create_table "cards", force: :cascade do |t|
|
||||
t.integer "collection_id", null: false
|
||||
t.integer "column_id"
|
||||
t.datetime "created_at", null: false
|
||||
t.integer "creator_id", null: false
|
||||
t.date "due_on"
|
||||
@@ -142,6 +150,7 @@ ActiveRecord::Schema[8.1].define(version: 2025_09_17_064006) do
|
||||
t.string "title"
|
||||
t.datetime "updated_at", null: false
|
||||
t.index ["collection_id"], name: "index_cards_on_collection_id"
|
||||
t.index ["column_id"], name: "index_cards_on_column_id"
|
||||
t.index ["last_active_at", "status"], name: "index_cards_on_last_active_at_and_status"
|
||||
t.index ["stage_id"], name: "index_cards_on_stage_id"
|
||||
end
|
||||
@@ -197,6 +206,15 @@ ActiveRecord::Schema[8.1].define(version: 2025_09_17_064006) do
|
||||
t.index ["filter_id"], name: "index_collections_filters_on_filter_id"
|
||||
end
|
||||
|
||||
create_table "columns", force: :cascade do |t|
|
||||
t.integer "collection_id", null: false
|
||||
t.string "color", null: false
|
||||
t.datetime "created_at", null: false
|
||||
t.string "name", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.index ["collection_id"], name: "index_columns_on_collection_id"
|
||||
end
|
||||
|
||||
create_table "comments", force: :cascade do |t|
|
||||
t.integer "card_id", null: false
|
||||
t.datetime "created_at", null: false
|
||||
@@ -237,14 +255,12 @@ ActiveRecord::Schema[8.1].define(version: 2025_09_17_064006) do
|
||||
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.bigint "auto_postpone_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", "auto_postpone_period"], name: "idx_on_container_type_container_id_auto_postpone_pe_47f82c5b73"
|
||||
t.index ["container_type", "container_id"], name: "index_entropy_configurations_on_container", unique: true
|
||||
end
|
||||
|
||||
@@ -508,11 +524,14 @@ ActiveRecord::Schema[8.1].define(version: 2025_09_17_064006) do
|
||||
add_foreign_key "ai_quotas", "users"
|
||||
add_foreign_key "card_activity_spikes", "cards"
|
||||
add_foreign_key "card_goldnesses", "cards"
|
||||
add_foreign_key "card_not_nows", "cards"
|
||||
add_foreign_key "cards", "columns"
|
||||
add_foreign_key "cards", "workflow_stages", column: "stage_id"
|
||||
add_foreign_key "closures", "cards"
|
||||
add_foreign_key "closures", "users"
|
||||
add_foreign_key "collection_publications", "collections"
|
||||
add_foreign_key "collections", "workflows"
|
||||
add_foreign_key "columns", "collections"
|
||||
add_foreign_key "comments", "cards"
|
||||
add_foreign_key "conversation_messages", "conversations"
|
||||
add_foreign_key "conversations", "users"
|
||||
|
||||
+89
-32
@@ -433,8 +433,23 @@ columns:
|
||||
- *5
|
||||
- *6
|
||||
- *9
|
||||
card_not_nows:
|
||||
- *23
|
||||
- *5
|
||||
- *6
|
||||
- *9
|
||||
cards:
|
||||
- *24
|
||||
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
||||
auto_increment:
|
||||
name: column_id
|
||||
cast_type: *3
|
||||
sql_type_metadata: *4
|
||||
'null': true
|
||||
default:
|
||||
default_function:
|
||||
collation:
|
||||
comment:
|
||||
- *5
|
||||
- &25 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
||||
auto_increment:
|
||||
@@ -610,6 +625,22 @@ columns:
|
||||
collections_filters:
|
||||
- *24
|
||||
- *20
|
||||
columns:
|
||||
- *24
|
||||
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
||||
auto_increment:
|
||||
name: color
|
||||
cast_type: *7
|
||||
sql_type_metadata: *8
|
||||
'null': false
|
||||
default:
|
||||
default_function:
|
||||
collation:
|
||||
comment:
|
||||
- *5
|
||||
- *6
|
||||
- *10
|
||||
- *9
|
||||
comments:
|
||||
- *23
|
||||
- *5
|
||||
@@ -741,17 +772,7 @@ columns:
|
||||
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
|
||||
name: auto_postpone_period
|
||||
cast_type: *11
|
||||
sql_type_metadata: *12
|
||||
'null': false
|
||||
@@ -1488,6 +1509,7 @@ primary_keys:
|
||||
card_activity_spikes: id
|
||||
card_engagements: id
|
||||
card_goldnesses: id
|
||||
card_not_nows: id
|
||||
cards: id
|
||||
closers_filters:
|
||||
closure_reasons: id
|
||||
@@ -1495,6 +1517,7 @@ primary_keys:
|
||||
collection_publications: id
|
||||
collections: id
|
||||
collections_filters:
|
||||
columns: id
|
||||
comments: id
|
||||
conversation_messages: id
|
||||
conversations: id
|
||||
@@ -1542,6 +1565,7 @@ data_sources:
|
||||
card_activity_spikes: true
|
||||
card_engagements: true
|
||||
card_goldnesses: true
|
||||
card_not_nows: true
|
||||
cards: true
|
||||
closers_filters: true
|
||||
closure_reasons: true
|
||||
@@ -1549,6 +1573,7 @@ data_sources:
|
||||
collection_publications: true
|
||||
collections: true
|
||||
collections_filters: true
|
||||
columns: true
|
||||
comments: true
|
||||
conversation_messages: true
|
||||
conversations: true
|
||||
@@ -1972,6 +1997,23 @@ indexes:
|
||||
nulls_not_distinct:
|
||||
comment:
|
||||
valid: true
|
||||
card_not_nows:
|
||||
- !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition
|
||||
table: card_not_nows
|
||||
name: index_card_not_nows_on_card_id
|
||||
unique: true
|
||||
columns:
|
||||
- card_id
|
||||
lengths: {}
|
||||
orders: {}
|
||||
opclasses: {}
|
||||
where:
|
||||
type:
|
||||
using:
|
||||
include:
|
||||
nulls_not_distinct:
|
||||
comment:
|
||||
valid: true
|
||||
cards:
|
||||
- !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition
|
||||
table: cards
|
||||
@@ -1989,6 +2031,22 @@ indexes:
|
||||
nulls_not_distinct:
|
||||
comment:
|
||||
valid: true
|
||||
- !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition
|
||||
table: cards
|
||||
name: index_cards_on_column_id
|
||||
unique: false
|
||||
columns:
|
||||
- column_id
|
||||
lengths: {}
|
||||
orders: {}
|
||||
opclasses: {}
|
||||
where:
|
||||
type:
|
||||
using:
|
||||
include:
|
||||
nulls_not_distinct:
|
||||
comment:
|
||||
valid: true
|
||||
- !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition
|
||||
table: cards
|
||||
name: index_cards_on_last_active_at_and_status
|
||||
@@ -2205,6 +2263,23 @@ indexes:
|
||||
nulls_not_distinct:
|
||||
comment:
|
||||
valid: true
|
||||
columns:
|
||||
- !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition
|
||||
table: columns
|
||||
name: index_columns_on_collection_id
|
||||
unique: false
|
||||
columns:
|
||||
- collection_id
|
||||
lengths: {}
|
||||
orders: {}
|
||||
opclasses: {}
|
||||
where:
|
||||
type:
|
||||
using:
|
||||
include:
|
||||
nulls_not_distinct:
|
||||
comment:
|
||||
valid: true
|
||||
comments:
|
||||
- !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition
|
||||
table: comments
|
||||
@@ -2292,30 +2367,12 @@ indexes:
|
||||
entropy_configurations:
|
||||
- !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition
|
||||
table: entropy_configurations
|
||||
name: idx_on_container_type_container_id_auto_close_perio_74dc880875
|
||||
name: idx_on_container_type_container_id_auto_postpone_pe_47f82c5b73
|
||||
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
|
||||
- auto_postpone_period
|
||||
lengths: {}
|
||||
orders: {}
|
||||
opclasses: {}
|
||||
@@ -3191,4 +3248,4 @@ indexes:
|
||||
comment:
|
||||
valid: true
|
||||
workflows: []
|
||||
version: 20250917064006
|
||||
version: 20250924124737
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
#!/usr/bin/env ruby
|
||||
|
||||
require_relative "../../config/environment"
|
||||
|
||||
ApplicationRecord.with_each_tenant do |tenant|
|
||||
puts "Processing tenant: #{tenant}"
|
||||
|
||||
Collection.find_each do |collection|
|
||||
next unless collection.workflow.present?
|
||||
|
||||
# Map to track stage_id -> column
|
||||
columns_by_stage = {}
|
||||
|
||||
# Create columns from workflow stages
|
||||
collection.workflow.stages.find_each do |stage|
|
||||
column = collection.columns.create!(
|
||||
name: stage.name,
|
||||
color: stage.color || Card::Colored::COLORS.first
|
||||
)
|
||||
columns_by_stage[stage] = column
|
||||
puts "Created column '#{column.name}' for collection '#{collection.name}'"
|
||||
end
|
||||
|
||||
# Associate cards with their corresponding columns based on stages
|
||||
collection.cards.includes(:stage).find_each do |card|
|
||||
next unless card.stage.present?
|
||||
|
||||
unless card.stage.workflow.collections.include?(collection)
|
||||
puts "Corrupt data: the card with id #{card.id} has the stage #{card.stage.name} with id #{card.stage.id} that belongs to a workflow not asociated ot its collection"
|
||||
next
|
||||
end
|
||||
|
||||
stage = columns_by_stage[card.stage]
|
||||
card.update!(column: stage)
|
||||
puts "Associated card ##{card.id} with column '#{stage.name}'"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
puts "Migration completed!"
|
||||
@@ -6,10 +6,9 @@ class Account::EntropyConfigurationsControllerTest < ActionDispatch::Integration
|
||||
end
|
||||
|
||||
test "update" do
|
||||
put account_entropy_configuration_path, params: { entropy_configuration: { auto_close_period: 1.day, auto_reconsider_period: 2.days } }
|
||||
put account_entropy_configuration_path, params: { entropy_configuration: { auto_postpone_period: 1.day } }
|
||||
|
||||
assert_equal 1.day, entropy_configurations("37s_account").auto_close_period
|
||||
assert_equal 2.days, entropy_configurations("37s_account").auto_reconsider_period
|
||||
assert_equal 1.day, entropy_configurations("37s_account").auto_postpone_period
|
||||
|
||||
assert_redirected_to account_settings_path
|
||||
end
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
require "test_helper"
|
||||
|
||||
class Cards::DropsControllerTest < ActionDispatch::IntegrationTest
|
||||
setup do
|
||||
sign_in_as :kevin
|
||||
@card = cards(:logo)
|
||||
end
|
||||
|
||||
test "drop to considering" do
|
||||
assert_changes -> { @card.reload.considering? }, from: false, to: true do
|
||||
post cards_drops_path, params: { dropped_item_id: @card.id, drop_target: "considering" }, as: :turbo_stream
|
||||
assert_column_rerendered("considering")
|
||||
end
|
||||
end
|
||||
|
||||
test "drop to doing" do
|
||||
@card = cards(:text)
|
||||
|
||||
assert_changes -> { @card.reload.doing? }, from: false, to: true do
|
||||
post cards_drops_path, params: { dropped_item_id: @card.id, drop_target: "doing" }, as: :turbo_stream
|
||||
assert_column_rerendered("doing")
|
||||
end
|
||||
end
|
||||
|
||||
test "invalid drop target" do
|
||||
post cards_drops_path, params: { dropped_item_id: @card.id, drop_target: "invalid" }, as: :turbo_stream
|
||||
assert_response :bad_request
|
||||
end
|
||||
|
||||
private
|
||||
def assert_column_rerendered(target)
|
||||
assert_turbo_stream action: :replace, target: "#{target}-cards"
|
||||
end
|
||||
end
|
||||
@@ -1,39 +0,0 @@
|
||||
require "test_helper"
|
||||
|
||||
class Cards::EngagementsControllerTest < ActionDispatch::IntegrationTest
|
||||
setup do
|
||||
sign_in_as :kevin
|
||||
end
|
||||
|
||||
test "create" do
|
||||
card = cards(:text)
|
||||
|
||||
assert_changes -> { card.reload.doing? }, from: false, to: true do
|
||||
post card_engagement_path(card), params: { engagement: "doing" }
|
||||
assert_card_container_rerendered(card)
|
||||
end
|
||||
end
|
||||
|
||||
test "create on_deck" do
|
||||
card = cards(:text)
|
||||
|
||||
assert_changes -> { card.reload.on_deck? }, from: false, to: true do
|
||||
post card_engagement_path(card), params: { engagement: "on_deck" }
|
||||
assert_card_container_rerendered(card)
|
||||
end
|
||||
end
|
||||
|
||||
test "destroy" do
|
||||
card = cards(:logo)
|
||||
|
||||
assert_changes -> { card.reload.doing? }, from: true, to: false do
|
||||
delete card_engagement_path(card)
|
||||
assert_card_container_rerendered(card)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def assert_card_container_rerendered(card)
|
||||
assert_turbo_stream action: :replace, target: dom_id(card, :card_container)
|
||||
end
|
||||
end
|
||||
@@ -7,10 +7,9 @@ class Collections::EntropyConfigurationsControllerTest < ActionDispatch::Integra
|
||||
end
|
||||
|
||||
test "update" do
|
||||
put collection_entropy_configuration_path(@collection), params: { collection: { auto_close_period: 1.day, auto_reconsider_period: 2.days } }
|
||||
put collection_entropy_configuration_path(@collection), params: { collection: { auto_postpone_period: 1.day } }
|
||||
|
||||
assert_equal 1.day, @collection.entropy_configuration.reload.auto_close_period
|
||||
assert_equal 2.days, @collection.entropy_configuration.reload.auto_reconsider_period
|
||||
assert_equal 1.day, @collection.entropy_configuration.reload.auto_postpone_period
|
||||
|
||||
assert_turbo_stream action: :replace, target: dom_id(@collection, :entropy_configuration)
|
||||
end
|
||||
|
||||
@@ -31,8 +31,7 @@ class CollectionsControllerTest < ActionDispatch::IntegrationTest
|
||||
collection: {
|
||||
name: "Writebook bugs",
|
||||
all_access: false,
|
||||
auto_close_period: 1.day,
|
||||
auto_reconsider_period: 2.days
|
||||
auto_postpone_period: 1.day
|
||||
},
|
||||
user_ids: users(:kevin, :jz).pluck(:id)
|
||||
}
|
||||
@@ -40,8 +39,7 @@ class CollectionsControllerTest < ActionDispatch::IntegrationTest
|
||||
assert_redirected_to edit_collection_path(collections(:writebook))
|
||||
assert_equal "Writebook bugs", collections(:writebook).reload.name
|
||||
assert_equal users(:kevin, :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_equal 1.day, entropy_configurations(:writebook_collection).auto_postpone_period
|
||||
assert_not collections(:writebook).all_access?
|
||||
end
|
||||
|
||||
|
||||
@@ -7,38 +7,4 @@ class Public::Collections::CardPreviewsControllerTest < ActionDispatch::Integrat
|
||||
collections(:writebook).publish
|
||||
end
|
||||
|
||||
test "render considering cards" do
|
||||
assert cards(:text).considering?
|
||||
assert_not cards(:logo).considering?
|
||||
|
||||
get public_collection_card_previews_path(collections(:writebook).publication.key, target: "considering-cards", format: :turbo_stream)
|
||||
|
||||
assert_select ".card", text: /#{cards(:text).title}/
|
||||
assert_select ".card", text: cards(:logo).title, count: 0
|
||||
end
|
||||
|
||||
test "render doing cards" do
|
||||
assert cards(:logo).doing?
|
||||
assert_not cards(:text).doing?
|
||||
|
||||
get public_collection_card_previews_path(collections(:writebook).publication.key, target: "doing-cards", format: :turbo_stream)
|
||||
|
||||
assert_select ".card", text: /#{cards(:logo).title}/
|
||||
assert_select ".card", text: cards(:text).title, count: 0
|
||||
end
|
||||
|
||||
test "render closed cards" do
|
||||
assert cards(:shipping).closed?
|
||||
assert_not cards(:text).doing?
|
||||
|
||||
get public_collection_card_previews_path(collections(:writebook).publication.key, target: "closed-cards", format: :turbo_stream)
|
||||
|
||||
assert_select ".card", text: /#{cards(:shipping).title}/
|
||||
assert_select ".card", text: cards(:text).title, count: 0
|
||||
end
|
||||
|
||||
test "bad response for unknown target" do
|
||||
get public_collection_card_previews_path(collections(:writebook).publication.key, format: :turbo_stream)
|
||||
assert_response :bad_request
|
||||
end
|
||||
end
|
||||
|
||||
Vendored
+4
-4
@@ -1,7 +1,7 @@
|
||||
logo:
|
||||
collection: writebook
|
||||
creator: david
|
||||
stage: qa_triage
|
||||
column: writebook_triage
|
||||
title: The logo isn't big enough
|
||||
due_on: <%= 3.days.from_now %>
|
||||
created_at: <%= 1.week.ago %>
|
||||
@@ -11,7 +11,7 @@ logo:
|
||||
layout:
|
||||
collection: writebook
|
||||
creator: david
|
||||
stage: qa_triage
|
||||
column: writebook_triage
|
||||
title: Layout is broken
|
||||
created_at: <%= 1.week.ago %>
|
||||
status: published
|
||||
@@ -20,16 +20,16 @@ layout:
|
||||
text:
|
||||
collection: writebook
|
||||
creator: kevin
|
||||
column: writebook_in_progress
|
||||
title: The text is too small
|
||||
created_at: <%= 1.week.ago %>
|
||||
status: published
|
||||
last_active_at: <%= 1.week.ago %>
|
||||
stage: qa_in_progress
|
||||
|
||||
shipping:
|
||||
collection: writebook
|
||||
creator: kevin
|
||||
stage: qa_triage
|
||||
column: writebook_triage
|
||||
title: We need to ship the app
|
||||
created_at: <%= 1.week.ago %>
|
||||
status: published
|
||||
|
||||
Vendored
-1
@@ -2,7 +2,6 @@ writebook:
|
||||
name: Writebook
|
||||
creator: david
|
||||
all_access: true
|
||||
workflow: qa
|
||||
|
||||
private:
|
||||
name: Private collection
|
||||
|
||||
Vendored
+20
@@ -0,0 +1,20 @@
|
||||
# Columns for writebook collection (which has qa workflow)
|
||||
writebook_triage:
|
||||
name: Triage
|
||||
color: "#000000"
|
||||
collection: writebook
|
||||
|
||||
writebook_in_progress:
|
||||
name: In progress
|
||||
color: "#3b3633"
|
||||
collection: writebook
|
||||
|
||||
writebook_on_hold:
|
||||
name: On Hold
|
||||
color: "#67695e"
|
||||
collection: writebook
|
||||
|
||||
writebook_review:
|
||||
name: Review
|
||||
color: "#eb7a32"
|
||||
collection: writebook
|
||||
+3
-6
@@ -1,14 +1,11 @@
|
||||
37s_account:
|
||||
container: 37s (Account)
|
||||
auto_reconsider_period: <%= 30.days.to_i %>
|
||||
auto_close_period: <%= 30.days.to_i %>
|
||||
auto_postpone_period: <%= 30.days.to_i %>
|
||||
|
||||
writebook_collection:
|
||||
container: writebook (Collection)
|
||||
auto_reconsider_period: <%= 90.days.to_i %>
|
||||
auto_close_period: <%= 90.days.to_i %>
|
||||
auto_postpone_period: <%= 90.days.to_i %>
|
||||
|
||||
private_collection:
|
||||
container: private (Collection)
|
||||
auto_reconsider_period: <%= 30.days.to_i %>
|
||||
auto_close_period: <%= 30.days.to_i %>
|
||||
auto_postpone_period: <%= 30.days.to_i %>
|
||||
|
||||
@@ -1,16 +1,12 @@
|
||||
require "test_helper"
|
||||
|
||||
class Card::ColoredTest < ActiveSupport::TestCase
|
||||
test "use default color no stage" do
|
||||
test "use default color if no column" do
|
||||
cards(:logo).update! column: nil
|
||||
assert_equal Card::DEFAULT_COLOR, cards(:logo).color
|
||||
end
|
||||
|
||||
test "use default color not in doing stage" do
|
||||
assert_equal Card::DEFAULT_COLOR, cards(:text).color
|
||||
end
|
||||
|
||||
test "infer color from stage when in doing stage" do
|
||||
cards(:text).engage
|
||||
assert_equal cards(:text).stage.color, cards(:text).color
|
||||
test "infer color from column" do
|
||||
assert_equal cards(:layout).column.color, cards(:layout).color
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
require "test_helper"
|
||||
|
||||
class Card::EngageableTest < ActiveSupport::TestCase
|
||||
setup do
|
||||
Current.session = sessions(:david)
|
||||
end
|
||||
|
||||
test "check the engagement status of a card" do
|
||||
assert cards(:logo).doing?
|
||||
assert_not cards(:text).doing?
|
||||
|
||||
assert_not cards(:logo).considering?
|
||||
assert cards(:text).considering?
|
||||
|
||||
assert_not cards(:logo).on_deck?
|
||||
assert_not cards(:text).on_deck?
|
||||
|
||||
assert_equal "doing", cards(:logo).engagement_status
|
||||
assert_equal "considering", cards(:text).engagement_status
|
||||
end
|
||||
|
||||
test "change the engagement" do
|
||||
assert_changes -> { cards(:text).reload.doing? }, to: true do
|
||||
cards(:text).engage
|
||||
end
|
||||
|
||||
assert_changes -> { cards(:logo).reload.doing? }, to: false do
|
||||
cards(:logo).reconsider
|
||||
end
|
||||
end
|
||||
|
||||
test "engaging with closed cards" do
|
||||
cards(:text).close
|
||||
|
||||
assert_not cards(:text).considering?
|
||||
assert_not cards(:text).doing?
|
||||
assert_not cards(:text).on_deck?
|
||||
|
||||
cards(:text).engage
|
||||
assert_not cards(:text).reload.closed?
|
||||
assert cards(:text).doing?
|
||||
|
||||
cards(:text).close
|
||||
cards(:text).reconsider
|
||||
assert_not cards(:text).reload.closed?
|
||||
assert cards(:text).considering?
|
||||
end
|
||||
|
||||
test "scopes" do
|
||||
assert_includes Card.doing, cards(:logo)
|
||||
assert_not_includes Card.doing, cards(:text)
|
||||
|
||||
assert_includes Card.considering, cards(:text)
|
||||
assert_not_includes Card.considering, cards(:logo)
|
||||
|
||||
cards(:text).move_to_on_deck
|
||||
assert_includes Card.on_deck, cards(:text)
|
||||
assert_not_includes Card.on_deck, cards(:logo)
|
||||
end
|
||||
end
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user