Remove "save as draft"

Removes the creating status completely as well as the abandoned cards system. It will
always resume drafts if they exist, for a given collection and user.

https://app.box-car.com/5986089/cards/2489
This commit is contained in:
Jorge Manrubia
2025-11-02 11:20:16 +01:00
parent efd5c1dba2
commit 9d4dd3b00e
19 changed files with 35 additions and 125 deletions
@@ -1,7 +0,0 @@
class Cards::RecoversController < ApplicationController
include CardScoped
def create
redirect_to @card.recover_abandoned_creation
end
end
+2 -2
View File
@@ -9,7 +9,7 @@ class CardsController < ApplicationController
end
def create
card = @collection.cards.create!
card = @collection.cards.find_or_create_by!(creator: Current.user, status: "drafted")
redirect_to card
end
@@ -30,7 +30,7 @@ class CardsController < ApplicationController
def destroy
@card.destroy!
redirect_to @card.collection, notice: ("Card deleted" unless @card.creating?)
redirect_to @card.collection, notice: "Card deleted"
end
private
@@ -1,7 +0,0 @@
class RemoveAbandonedCreationsJob < ApplicationJob
def perform
ApplicationRecord.with_each_tenant do |tenant|
Card.remove_abandoned_creations
end
end
end
+1 -22
View File
@@ -2,38 +2,17 @@ module Card::Statuses
extend ActiveSupport::Concern
included do
enum :status, %w[ creating drafted published ].index_by(&:itself)
enum :status, %w[ drafted published ].index_by(&:itself)
after_create -> { track_event :published }, if: :published?
scope :published_or_drafted_by, ->(user) { where(status: :published).or(where(status: :drafted, creator: user)) }
end
class_methods do
def remove_abandoned_creations
Card.creating.where(updated_at: ..1.day.ago).destroy_all
end
end
def can_recover_abandoned_creation?
abandoned_creations.where(updated_at: 1.day.ago..).any?
end
def recover_abandoned_creation
abandoned_creations.last.tap do |card|
Card.creating.where(creator: creator).excluding(card).destroy_all
end
end
def publish
transaction do
published!
track_event :published
end
end
private
def abandoned_creations
Card.creating.where(creator: creator).where("created_at != updated_at").excluding(self)
end
end
+1 -1
View File
@@ -38,7 +38,7 @@
<% if card.published? %>
<%= render "cards/container/footer/published", card: card %>
<% elsif card.creating? %>
<% elsif card.drafted? %>
<%= render "cards/container/footer/draft", card: card %>
<% end %>
</section>
@@ -1,12 +1,3 @@
<% if card.creating? && card.can_recover_abandoned_creation? %>
<div class="card__banner min-width max-width" style="--card-color: <%= card.color %>;">
<span class="overflow-ellipsis">You have an unsaved card. Would you like to continue where you left off?</span>
<%= button_to card_recover_path(card), class: "btn txt-small", data: { turbo_action: "replace" } do %>
<span>Restore</span>
<% end %>
</div>
<% end %>
<% if card.drafted? %>
<div class="card__banner min-width max-width" style="--card-color: <%= card.color %>;">
<span class="overflow-ellipsis">This is a draft, its only visible to you.</span>
@@ -3,6 +3,4 @@
<%= button_to "Create and add another", card_publish_path(card), method: :post, class: "btn btn--reversed", name: "creation_type", value: "add_another",
title: "Create and add another (ctrl+shift+enter)", form: { data: { turbo: false } }, data: { controller: "hotkey", action: "keydown.ctrl+shift+enter@document->hotkey#click" } %>
<%= button_to "Save as a draft", card_path(card), name: "card[status]", value: "drafted", method: :put, class: "btn btn--reversed" %>
</div>
@@ -15,13 +15,9 @@
</span>
<span class="card__meta-text card__meta-text--updated overflow-ellipsis">
<% if card.creating? %>
&nbsp;
<% else %>
<%= icon_tag "refresh--meta" %>
Updated
<%= local_datetime_tag(card.last_active_at, style: :daysago) %>
<% end %>
<%= icon_tag "refresh--meta" %>
Updated
<%= local_datetime_tag(card.last_active_at, style: :daysago) %>
</span>
<span class="card__meta-text card__meta-text--assignees overflow-ellipsis">
+1 -1
View File
@@ -10,7 +10,7 @@
</dialog>
</div>
<% if card.tags.any? || card.creating? %>
<% if card.tags.any? %>
<div class="min-width overflow-ellipsis">
<% card.tags.each_with_index do |tag, index| %>
<%= link_to cards_path(collection_ids: [ card.collection ], tag_ids: [ tag.id ]),
+1 -1
View File
@@ -15,7 +15,7 @@
<div data-controller="beacon lightbox" data-beacon-url-value="<%= card_reading_path(@card) %>">
<%= render "cards/container", card: @card %>
<%= render "cards/messages", card: @card unless @card.creating? %>
<%= render "cards/messages", card: @card unless @card.drafted? %>
<%= render "layouts/lightbox" do %>
<%= button_to_remove_card_image(@card) if @card.image.attached? %>
-3
View File
@@ -8,9 +8,6 @@ production: &production
auto_postpone_all_due:
class: Card::AutoPostponeAllDueJob
schedule: every hour at minute 50
remove_abandoned_creations:
class: RemoveAbandonedCreationsJob
schedule: every hour at minute 59
delete_unused_tags:
class: DeleteUnusedTagsJob
schedule: every day at 04:02
-1
View File
@@ -71,7 +71,6 @@ Rails.application.routes.draw do
resource :triage
resource :publish
resource :reading
resource :recover
resource :watch
resource :collection
resource :column
@@ -0,0 +1,5 @@
class ChangeCardsStatusDefaultToDrafted < ActiveRecord::Migration[8.2]
def change
change_column_default :cards, :status, from: "creating", to: "drafted"
end
end
Generated
+1 -1
View File
@@ -144,7 +144,7 @@ ActiveRecord::Schema[8.2].define(version: 2025_11_02_115338) do
t.integer "creator_id", null: false
t.date "due_on"
t.datetime "last_active_at", null: false
t.text "status", default: "creating", null: false
t.text "status", default: "drafted", null: false
t.string "title"
t.datetime "updated_at", null: false
t.index ["collection_id"], name: "index_cards_on_collection_id"
+1 -1
View File
@@ -496,7 +496,7 @@ columns:
cast_type: *15
sql_type_metadata: *16
'null': false
default: creating
default: drafted
default_function:
collation:
comment:
@@ -21,11 +21,12 @@ class Cards::PublishesControllerTest < ActionDispatch::IntegrationTest
card.drafted!
assert_changes -> { card.reload.published? }, from: false, to: true do
assert_difference -> { Card.creating.count }, +1 do
assert_difference -> { Card.count }, +1 do
post card_publish_path(card, creation_type: "add_another")
end
end
assert_redirected_to Card.creating.last
assert Card.last.drafted?
assert_redirected_to Card.last
end
end
+14 -2
View File
@@ -15,11 +15,23 @@ class CardsControllerTest < ActionDispatch::IntegrationTest
assert_response :success
end
test "create" do
test "create a new draft" do
assert_difference -> { Card.count }, 1 do
post collection_cards_path(collections(:writebook))
end
assert_redirected_to card_path(Card.last)
assert Card.last.drafted?
assert_redirected_to Card.last
end
test "create resumes existing draft if it exists" do
draft = collections(:writebook).cards.create!(creator: users(:kevin), status: :drafted)
assert_no_difference -> { Card.count } do
post collection_cards_path(collections(:writebook))
end
assert_redirected_to draft
end
test "show" do
@@ -1,18 +0,0 @@
require "test_helper"
class Cards::RecoversControllerTest < ActionDispatch::IntegrationTest
setup do
sign_in_as :jz
end
test "create" do
abandoned_card = collections(:writebook).cards.create! creator: users(:kevin)
abandoned_card.update!(title: "An edited title")
unsaved_card = collections(:writebook).cards.create! creator: users(:kevin)
post card_recover_path(unsaved_card)
assert_redirected_to abandoned_card
assert_equal [ abandoned_card ], Card.creating
end
end
+2 -38
View File
@@ -1,12 +1,10 @@
require "test_helper"
class Card::StatusesTest < ActiveSupport::TestCase
test "cards start out in a `creating` state" do
test "cards start out in a `drafted` state" do
card = collections(:writebook).cards.create! creator: users(:kevin), title: "Newly created card"
assert card.creating?
assert_not_includes Card.published_or_drafted_by(users(:kevin)), card
assert_not_includes Card.published_or_drafted_by(users(:jz)), card
assert card.drafted?
end
test "cards are only visible to the creator when drafted" do
@@ -51,38 +49,4 @@ class Card::StatusesTest < ActiveSupport::TestCase
assert_equal card, Event.last.eventable
assert_equal "card_published", Event.last.action
end
test "can_recover_abandoned_creation?" do
card = collections(:writebook).cards.create! creator: users(:kevin)
unsaved_card = collections(:writebook).cards.new creator: users(:kevin)
assert_not unsaved_card.can_recover_abandoned_creation?
card.update!(title: "Something worth keeping")
assert unsaved_card.can_recover_abandoned_creation?
end
test "recover_abandoned_creation" do
card_edited = collections(:writebook).cards.create! creator: users(:kevin)
card_edited.update!(title: "Something worth keeping")
card_not_edited = collections(:writebook).cards.create! creator: users(:kevin)
assert card_not_edited.can_recover_abandoned_creation?
assert_equal card_edited, card_not_edited.recover_abandoned_creation
assert_raises(ActiveRecord::RecordNotFound) { card_not_edited.reload }
end
test "remove_abandoned_creations" do
card_old = collections(:writebook).cards.create! creator: users(:kevin), updated_at: 2.days.ago
card_recent = collections(:writebook).cards.create! creator: users(:kevin)
assert_equal 2, Card.creating.count
Card.remove_abandoned_creations
assert_equal [ card_recent ], Card.creating
end
end