diff --git a/Gemfile.lock b/Gemfile.lock
index 3931a7690..696e77ae4 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -42,9 +42,9 @@ GIT
GIT
remote: https://github.com/crmne/ruby_llm.git
- revision: 74305703dcdcdcf8b2adefd8236184bcffc83910
+ revision: a35aa111a0bb36b7f0363b1f1f2a25e3d5378fd3
specs:
- ruby_llm (1.3.0rc1)
+ ruby_llm (1.3.1)
base64
event_stream_parser (~> 1)
faraday (>= 1.10.0)
@@ -56,7 +56,7 @@ GIT
GIT
remote: https://github.com/rails/rails.git
- revision: 0ac3fba6ee5f7d1680024a4925eee6069095487b
+ revision: fd49d6a3180244969fb5ab9768d0bc7d9e4df9fe
branch: main
specs:
actioncable (8.1.0.alpha)
@@ -162,29 +162,29 @@ GEM
addressable (2.8.7)
public_suffix (>= 2.0.2, < 7.0)
ast (2.4.3)
- aws-eventstream (1.3.2)
- aws-partitions (1.1109.0)
- aws-sdk-core (3.224.1)
+ aws-eventstream (1.4.0)
+ aws-partitions (1.1115.0)
+ aws-sdk-core (3.225.2)
aws-eventstream (~> 1, >= 1.3.0)
aws-partitions (~> 1, >= 1.992.0)
aws-sigv4 (~> 1.9)
base64
jmespath (~> 1, >= 1.6.1)
logger
- aws-sdk-kms (1.101.0)
- aws-sdk-core (~> 3, >= 3.216.0)
+ aws-sdk-kms (1.104.0)
+ aws-sdk-core (~> 3, >= 3.225.0)
aws-sigv4 (~> 1.5)
- aws-sdk-s3 (1.188.0)
- aws-sdk-core (~> 3, >= 3.224.1)
+ aws-sdk-s3 (1.189.1)
+ aws-sdk-core (~> 3, >= 3.225.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.5)
- aws-sigv4 (1.11.0)
+ aws-sigv4 (1.12.1)
aws-eventstream (~> 1, >= 1.0.2)
base64 (0.3.0)
bcrypt (3.1.20)
bcrypt_pbkdf (1.1.1)
benchmark (0.4.1)
- bigdecimal (3.2.1)
+ bigdecimal (3.2.2)
bootsnap (1.18.6)
msgpack (~> 1.2)
brakeman (7.0.2)
@@ -418,10 +418,10 @@ GEM
rexml (~> 3.2, >= 3.2.5)
rubyzip (>= 1.2.2, < 3.0)
websocket (~> 1.0)
- sentry-rails (5.24.0)
+ sentry-rails (5.25.0)
railties (>= 5.0)
- sentry-ruby (~> 5.24.0)
- sentry-ruby (5.24.0)
+ sentry-ruby (~> 5.25.0)
+ sentry-ruby (5.25.0)
bigdecimal
concurrent-ruby (~> 1.0, >= 1.0.2)
solid_cable (3.0.8)
@@ -445,11 +445,11 @@ GEM
sqlite-vec (0.1.7.alpha.2-x86_64-darwin)
sqlite-vec (0.1.7.alpha.2-x86_64-linux)
sqlite-vec (0.1.7.alpha.2-x86_64-mingw32)
- sqlite3 (2.6.0)
+ sqlite3 (2.7.0)
mini_portile2 (~> 2.8.0)
- sqlite3 (2.6.0-arm64-darwin)
- sqlite3 (2.6.0-x86_64-darwin)
- sqlite3 (2.6.0-x86_64-linux-gnu)
+ sqlite3 (2.7.0-arm64-darwin)
+ sqlite3 (2.7.0-x86_64-darwin)
+ sqlite3 (2.7.0-x86_64-linux-gnu)
sshkit (1.24.0)
base64
logger
@@ -471,7 +471,7 @@ GEM
tiktoken_ruby (0.0.11.1-x86_64-darwin)
tiktoken_ruby (0.0.11.1-x86_64-linux)
timeout (0.4.3)
- turbo-rails (2.0.13)
+ turbo-rails (2.0.16)
actionpack (>= 7.1.0)
railties (>= 7.1.0)
tzinfo (2.0.6)
diff --git a/app/assets/stylesheets/_global.css b/app/assets/stylesheets/_global.css
index c7e4cedd0..59a5b9357 100644
--- a/app/assets/stylesheets/_global.css
+++ b/app/assets/stylesheets/_global.css
@@ -160,6 +160,7 @@
--color-negative: oklch(var(--lch-red-dark));
--color-positive: oklch(var(--lch-green-dark));
--color-link: oklch(var(--lch-blue-dark));
+ --color-selected-light: oklch(var(--lch-blue-lightest));
--color-selected: oklch(var(--lch-blue-lighter));
--color-selected-dark: oklch(var(--lch-blue-light));
--color-highlight: oklch(var(--lch-yellow-lighter));
diff --git a/app/assets/stylesheets/card-columns.css b/app/assets/stylesheets/card-columns.css
index 15e8f03b0..de903045a 100644
--- a/app/assets/stylesheets/card-columns.css
+++ b/app/assets/stylesheets/card-columns.css
@@ -1,66 +1,73 @@
@layer components {
- /* Coluns
- /* -------------------------------------------------------------------------- */
+ /* Columns
+ /* ------------------------------------------------------------------------ */
/* Card contents are resized via container queries. We're using #main here as
the primary container so all cards have the same container reference */
#main:has(.card-columns) {
+ --cards-gap: min(2cqi, 1rem);
+ --gradient-border: linear-gradient(to right,var(--color-canvas), var(--color-ink-lighter) 3%, var(--color-ink-lighter) 97%, var(--color-canvas));
+ --reserved-bubble-space: calc(var(--bubble-size) + var(--bubble-gap));
+ --bubble-gap: 0.5rem;
+ --bubble-size: 4rem;
+
container-type: inline-size;
max-inline-size: 1400px;
+
+ @media (max-width: 639px) {
+ --bubble-gap: -0.5rem;
+ --bubble-size: 3rem;
+ --reserved-bubble-space: calc(var(--bubble-size) / 2 + var(--bubble-gap));
+ }
}
.card-columns {
- --bubble-size: 4rem;
- --bubble-gap: 0.5rem;
-
- display: grid;
- grid-template-columns: repeat(2, 50%);
margin: auto;
- max-inline-size: calc(100% - (var(--bubble-size) + var(--bubble-gap)) * 2);
+ max-inline-size: 100%;
position: relative;
&:before {
- background: linear-gradient(to right,var(--color-canvas), var(--color-ink-lighter) 3%, var(--color-ink-lighter) 97%, var(--color-canvas));
+ background: var(--gradient-border);
block-size: 1px;
content: "";
- display: block;
- inline-size: calc(100% + 1.2em);
inset: 0 -0.6em 100% -0.6em;
position: absolute;
}
- .cards:first-child {
- padding-inline-end: var(--cards-gap);
- position: relative;
-
- &:after {
- background: linear-gradient(var(--color-canvas), var(--color-ink-lighter) 1%, var(--color-ink-lighter) 99%, var(--color-canvas));
- block-size: calc(100% + 1.2em);
- content: "";
- display: block;
- inline-size: 1px;
- inset: -0.6em 0 -0.6em 100%;
- position: absolute;
- }
- }
-
- .cards:last-child {
- padding-inline-start: var(--cards-gap);
+ @media (min-width: 640px) {
+ display: grid;
+ grid-template-columns: repeat(2, 50%);
}
}
/* Cards
- /* -------------------------------------------------------------------------- */
+ /* ------------------------------------------------------------------------ */
.cards {
- --cards-gap: 2cqi;
-
- align-items: center;
+ align-items: stretch;
display: flex;
flex-direction: column;
gap: var(--cards-gap);
justify-content: start;
- padding-block: calc(var(--cards-gap) * 0.75);
+ padding: 0 var(--reserved-bubble-space) var(--cards-gap) var(--cards-gap);
+ position: relative;
+
+ /* Make sure filter dialogs don't end up behind sticky headers */
+ &:has([open]) {
+ z-index: 3;
+ }
+
+ &:first-child {
+ @media (min-width: 640px) {
+ border-inline-end: 1px solid var(--color-ink-lighter);
+ padding-inline: var(--reserved-bubble-space) var(--cards-gap);
+ }
+ }
+
+ @media (max-width: 639px) {
+ padding-block-end: calc(var(--cards-gap) * 2);
+ padding-inline-start: var(--reserved-bubble-space);
+ }
.card {
--block-space: 1em;
@@ -90,7 +97,7 @@
.workflow-stage:not(.workflow-stage--current) {
opacity: 0;
transition: opacity 0.2s ease-in-out;
-
+
@media (any-hover: hover) {
.card:hover & {
opacity: 1;
@@ -98,7 +105,7 @@
}
}
}
-
+
@media (any-hover: hover) {
.card:hover & {
img:not([src=""]) {
@@ -110,18 +117,46 @@
}
}
+ /* Column elements
+ /* ------------------------------------------------------------------------ */
+
.cards__heading {
- display: inline-flex;
+ background-color: var(--color-canvas);
+ display: flex;
font-size: var(--text-medium);
- inline-size: 100%;
+ inset-block-start: 0;
justify-content: center;
- text-transform: uppercase;
+ margin-block-start: 1px;
+ margin-inline: calc(-1 * var(--cards-gap) + 1px);
+ padding-block: var(--cards-gap);
+ position: sticky;
+ z-index: 2;
+
+ &, button {
+ font-weight: bold;
+ text-transform: uppercase;
+ }
+
+ button {
+ --hover-size: 0;
+
+ inline-size: auto;
+ margin-inline-start: 2em;
+ padding: 0 2em 0 0;
+ }
+
+ dialog {
+ font-weight: normal;
+ text-transform: none;
+ }
}
/* Considering
/* ------------------------------------------------------------------------ */
.cards--considering {
+ padding-inline-start: calc(var(--cards-gap) / 2);
+
.card {
--text-xx-large: 1.6em;
--text-small: 1.1em;
@@ -175,8 +210,21 @@
}
}
+ /* Doing
+ /* ------------------------------------------------------------------------ */
+
+ @media (max-width: 639px) {
+ .cards--doing:before {
+ background: var(--gradient-border);
+ block-size: 1px;
+ content: "";
+ inset: 0 0 auto;
+ position: absolute;
+ }
+ }
+
/* Closed
- /* -------------------------------------------------------------------------- */
+ /* ------------------------------------------------------------------------ */
.cards--closed {
justify-content: center;
@@ -187,22 +235,35 @@
position: relative;
&:before {
- background: linear-gradient(to right,var(--color-canvas), var(--color-ink-lighter) 3%, var(--color-ink-lighter) 97%, var(--color-canvas));
+ background: var(--gradient-border);
block-size: 1px;
content: "";
- display: block;
- inline-size: calc(100% + 1.2em);
inset: 0 -0.6em 100% -0.6em;
position: absolute;
}
- .card {
- inline-size: calc((100% - var(--cards-gap) * 2) / 3);
+ @media (max-width: 639px) {
+ flex-direction: column;
+ padding-inline: var(--reserved-bubble-space);
+ }
- @media (max-width: 960px) {
+ .cards__heading {
+ min-inline-size: 100%;
+
+ @media (max-width: 639px) {
+ margin-inline-end: calc(-1 * var(--cards-gap));
+ }
+ }
+
+ .card {
+ @media (min-width: 640px) {
inline-size: calc((100% - var(--cards-gap)) / 2);
}
+ @media (min-width: 960px) {
+ inline-size: calc((100% - var(--cards-gap) * 2) / 3);
+ }
+
&:nth-of-type(7) {
margin-block-start: var(--cards-gap);
}
@@ -229,7 +290,7 @@
aspect-ratio: unset;
border-width: 0.5ch;
font-size: 0.75em;
- inset: auto 0 auto auto;
+ inset: auto 0 var(--card-padding-block) auto;
transform: none;
}
@@ -248,4 +309,11 @@
}
}
}
+
+ /* Doing
+ /* ------------------------------------------------------------------------ */
+
+ .cards--doing {
+ padding-inline-end: calc(var(--cards-gap) / 2);
+ }
}
diff --git a/app/assets/stylesheets/card-filters.css b/app/assets/stylesheets/card-filters.css
deleted file mode 100644
index f7ea3605c..000000000
--- a/app/assets/stylesheets/card-filters.css
+++ /dev/null
@@ -1,34 +0,0 @@
-@layer components {
- .card-filter--column {
- > button {
- --hover-size: 0;
-
- inline-size: auto;
- padding: 0 2em 0 0;
- margin-inline-start: 2em;
- }
- }
-
- .card-filter--sorting {
- inset: auto 50% -4em;
- margin-inline-start: -1em;
- position: absolute;
- transform: translateX(-50%);
-
- #header:has(&) {
- position: relative;
- z-index: 1;
- }
- }
-
- .card-count__selector {
- inset: 0.5cqi 0.5cqi auto auto;
- position: absolute;
- text-align: start;
- z-index: 1;
-
- .popup {
- inset: 1em 1em auto auto;
- }
- }
-}
diff --git a/app/assets/stylesheets/card-perma.css b/app/assets/stylesheets/card-perma.css
index 4e2fd2678..9a4de11ed 100644
--- a/app/assets/stylesheets/card-perma.css
+++ b/app/assets/stylesheets/card-perma.css
@@ -183,6 +183,7 @@
.btn--reversed {
--btn-background: var(--color-canvas);
--btn-color: var(--card-color);
+ --btn-border-color: var(--color-container);
}
}
diff --git a/app/assets/stylesheets/cards.css b/app/assets/stylesheets/cards.css
index 892de566d..67cb0fce2 100644
--- a/app/assets/stylesheets/cards.css
+++ b/app/assets/stylesheets/cards.css
@@ -66,7 +66,9 @@
gap: var(--inline-space-half);
margin-block-start: calc(-1 * var(--card-padding-block));
margin-inline-start: calc(-1 * var(--card-padding-inline));
+ max-inline-size: 100%;
min-inline-size: 0;
+ overflow: visible;
}
.card__collection {
diff --git a/app/assets/stylesheets/drag_and_drop.css b/app/assets/stylesheets/drag_and_drop.css
new file mode 100644
index 000000000..48bf3dd4f
--- /dev/null
+++ b/app/assets/stylesheets/drag_and_drop.css
@@ -0,0 +1,15 @@
+@layer components {
+ .drag-and-drop__dragged-item {
+ box-shadow: none;
+ filter: grayscale(1) brightness(0.97);
+ opacity: 0.6;
+ outline: 2px dashed var(--color-selected-dark);
+ }
+
+ .drag-and-drop__hover-container {
+ background-color: var(--color-selected-light);
+ border-radius: 0.2em;
+ outline: 2px dashed var(--color-selected-dark);
+ transition: background-color 200ms;
+ }
+}
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index 20385eda0..af52c2fd6 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -1,5 +1,5 @@
class ApplicationController < ActionController::Base
- include Authentication, CurrentTimezone, SetPlatform
+ include Authentication, CurrentTimezone, SetPlatform, WriterAffinity
stale_when_importmap_changes
allow_browser versions: :modern
diff --git a/app/controllers/cards/drops_controller.rb b/app/controllers/cards/drops_controller.rb
new file mode 100644
index 000000000..25f7997a8
--- /dev/null
+++ b/app/controllers/cards/drops_controller.rb
@@ -0,0 +1,38 @@
+class Cards::DropsController < ApplicationController
+ include Collections::ColumnsScoped
+ before_action :set_card, :set_drop_target
+
+ def create
+ perform_drop_action
+ render_column_replacement
+ end
+
+ private
+ VALID_DROP_TARGETS = %w[ considering doing ]
+
+ 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 :doing
+ @card.engage
+ end
+ end
+
+ def render_column_replacement
+ page_and_filter = page_and_filter_for @filter.with(engagement_status: @drop_target.to_s), per_page: CardsController::PAGE_SIZE
+ render turbo_stream: turbo_stream.replace("#{@drop_target}-cards", method: :morph, partial: "cards/index/engagement/#{@drop_target}", locals: page_and_filter.to_h)
+ end
+end
diff --git a/app/controllers/cards/publishes_controller.rb b/app/controllers/cards/publishes_controller.rb
index f7ac6590c..298f8bbe4 100644
--- a/app/controllers/cards/publishes_controller.rb
+++ b/app/controllers/cards/publishes_controller.rb
@@ -3,6 +3,16 @@ class Cards::PublishesController < ApplicationController
def create
@card.publish
- redirect_to @card
+
+ if add_another_param?
+ redirect_to @collection.cards.create!, notice: "Card added"
+ else
+ redirect_to @card
+ end
end
+
+ private
+ def add_another_param?
+ params[:creation_type] == "add_another"
+ end
end
diff --git a/app/controllers/cards/readings_controller.rb b/app/controllers/cards/readings_controller.rb
index 2334cb8f5..a92c64525 100644
--- a/app/controllers/cards/readings_controller.rb
+++ b/app/controllers/cards/readings_controller.rb
@@ -1,6 +1,8 @@
class Cards::ReadingsController < ApplicationController
include CardScoped
+ skip_writer_affinity
+
def create
@notifications = @card.read_by(Current.user)
end
diff --git a/app/controllers/cards_controller.rb b/app/controllers/cards_controller.rb
index 8a8b4fb05..84627b625 100644
--- a/app/controllers/cards_controller.rb
+++ b/app/controllers/cards_controller.rb
@@ -1,7 +1,7 @@
require "ostruct"
class CardsController < ApplicationController
- include CollectionScoped, FilterScoped
+ include CollectionScoped, Collections::ColumnsScoped
skip_before_action :set_collection, only: :index
before_action :set_card, only: %i[ show edit update destroy ]
@@ -45,22 +45,6 @@ class CardsController < ApplicationController
@card = @collection.cards.find params[:id]
end
- def page_and_filter_for(filter, per_page: nil)
- cards = block_given? ? yield(filter.cards) : filter.cards
-
- OpenStruct.new \
- page: GearedPagination::Recordset.new(cards, per_page:).page(1),
- filter: filter
- end
-
- def page_and_filter_for_closed_cards
- if @filter.indexed_by.stalled?
- page_and_filter_for(@filter, per_page: PAGE_SIZE) { |cards| cards.recently_closed_first }
- else
- page_and_filter_for(@filter.with(indexed_by: "closed"), per_page: PAGE_SIZE) { |cards| cards.recently_closed_first }
- end
- end
-
def card_params
params.expect(card: [ :status, :title, :description, :image, tag_ids: [] ])
end
diff --git a/app/controllers/concerns/collections/columns_scoped.rb b/app/controllers/concerns/collections/columns_scoped.rb
new file mode 100644
index 000000000..a588a15b6
--- /dev/null
+++ b/app/controllers/concerns/collections/columns_scoped.rb
@@ -0,0 +1,23 @@
+module Collections::ColumnsScoped
+ extend ActiveSupport::Concern
+
+ included do
+ include FilterScoped
+ end
+
+ def page_and_filter_for(filter, per_page: nil)
+ cards = block_given? ? yield(filter.cards) : filter.cards
+
+ OpenStruct.new \
+ page: GearedPagination::Recordset.new(cards, per_page:).page(1),
+ filter: filter
+ end
+
+ def page_and_filter_for_closed_cards
+ if @filter.indexed_by.stalled?
+ page_and_filter_for(@filter, per_page: CardsController::PAGE_SIZE) { |cards| cards.recently_closed_first }
+ else
+ page_and_filter_for(@filter.with(indexed_by: "closed"), per_page: CardsController::PAGE_SIZE) { |cards| cards.recently_closed_first }
+ end
+ end
+end
diff --git a/app/controllers/concerns/filter_scoped.rb b/app/controllers/concerns/filter_scoped.rb
index 660365563..074dd967b 100644
--- a/app/controllers/concerns/filter_scoped.rb
+++ b/app/controllers/concerns/filter_scoped.rb
@@ -2,7 +2,7 @@ module FilterScoped
extend ActiveSupport::Concern
included do
- before_action :set_filter, only: :index
+ before_action :set_filter
end
private
diff --git a/app/controllers/concerns/writer_affinity.rb b/app/controllers/concerns/writer_affinity.rb
new file mode 100644
index 000000000..da03e1a42
--- /dev/null
+++ b/app/controllers/concerns/writer_affinity.rb
@@ -0,0 +1,14 @@
+module WriterAffinity
+ extend ActiveSupport::Concern
+
+ class_methods do
+ def skip_writer_affinity(**)
+ before_action :set_writer_affinity_opt_out_header, **
+ end
+ end
+
+ private
+ def set_writer_affinity_opt_out_header
+ response.headers["X-Writer-Affinity"] = "false"
+ end
+end
diff --git a/app/controllers/public/collections/card_previews_controller.rb b/app/controllers/public/collections/card_previews_controller.rb
index 936ba0825..f7572616a 100644
--- a/app/controllers/public/collections/card_previews_controller.rb
+++ b/app/controllers/public/collections/card_previews_controller.rb
@@ -17,8 +17,7 @@ class Public::Collections::CardPreviewsController < ApplicationController
when "closed-cards"
@collection.cards.closed.recently_closed_first
else
- head :bad_request
- Card.none
+ raise ActionController::BadRequest
end
end
end
diff --git a/app/helpers/cards_helper.rb b/app/helpers/cards_helper.rb
index a3aad18a2..5a00b13f4 100644
--- a/app/helpers/cards_helper.rb
+++ b/app/helpers/cards_helper.rb
@@ -65,7 +65,7 @@ module CardsHelper
title.join(" ")
end
- def cacheable_preview_parts_for(card)
- [ card, card.collection.workflow, card.collection.entropy_configuration ]
+ def cacheable_preview_parts_for(card, *options)
+ [ card, card.collection.workflow, card.collection.entropy_configuration, *options ]
end
end
diff --git a/app/javascript/application.js b/app/javascript/application.js
index 36d861cc7..725acdc82 100644
--- a/app/javascript/application.js
+++ b/app/javascript/application.js
@@ -5,3 +5,5 @@ import "controllers"
import "actiontext-lexical"
import "@rails/actiontext"
+
+window.fetch = Turbo.fetch // TODO: We need to make @rails/request.js use Turbo's fetch when it's present.
diff --git a/app/javascript/controllers/drag_and_drop_controller.js b/app/javascript/controllers/drag_and_drop_controller.js
new file mode 100644
index 000000000..01ccc593d
--- /dev/null
+++ b/app/javascript/controllers/drag_and_drop_controller.js
@@ -0,0 +1,81 @@
+import { Controller } from "@hotwired/stimulus"
+import { post } from "@rails/request.js"
+import { nextFrame } from "helpers/timing_helpers"
+
+export default class extends Controller {
+ static targets = [ "item", "container" ]
+ static values = { url: String }
+ static classes = [ "draggedItem", "hoverContainer" ]
+
+ // Actions
+
+ async dragStart(event) {
+ event.dataTransfer.effectAllowed = "move"
+ event.dataTransfer.dropEffect = "move"
+ event.dataTransfer.setData("37ui/move", event.target)
+
+ await nextFrame()
+ this.dragItem = this.#itemContaining(event.target)
+ this.sourceContainer = this.#containerContaining(this.dragItem)
+ this.dragItem.classList.add(this.draggedItemClass)
+ }
+
+ dragOver(event) {
+ event.preventDefault()
+ const container = this.#containerContaining(event.target)
+ this.#clearContainerHoverClasses()
+
+ if (!container) { return }
+
+ if (container !== this.sourceContainer) {
+ container.classList.add(this.hoverContainerClass)
+ }
+ }
+
+ async drop(event) {
+ const container = this.#containerContaining(event.target)
+
+ if (!container || container === this.sourceContainer) { return }
+
+ this.wasDropped = true
+
+ await this.#submitDropRequest(this.dragItem, container)
+ }
+
+ dragEnd() {
+ this.dragItem.classList.remove(this.draggedItemClass)
+ this.#clearContainerHoverClasses()
+
+ if (this.wasDropped) {
+ this.dragItem.remove()
+ }
+
+ this.sourceContainer = null
+ this.dragItem = null
+ this.wasDropped = false
+ }
+
+ #itemContaining(element) {
+ return this.itemTargets.find(item => item.contains(element) || item === element)
+ }
+
+ #containerContaining(element) {
+ return this.containerTargets.find(container => container.contains(element) || container === element)
+ }
+
+ #clearContainerHoverClasses() {
+ 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
+
+ body.append("dropped_item_id", id)
+ body.append("drop_target", containerTarget)
+ return post(this.urlValue, { body, headers: { Accept: "text/vnd.turbo-stream.html" } })
+ }
+}
diff --git a/app/views/cards/container/footer/_draft.html.erb b/app/views/cards/container/footer/_draft.html.erb
index f46e08b41..7b3dff00a 100644
--- a/app/views/cards/container/footer/_draft.html.erb
+++ b/app/views/cards/container/footer/_draft.html.erb
@@ -1,4 +1,5 @@
- <%= button_to "Create card", card_publish_path(card), class: "btn" %>
- <%= button_to "Save as draft", collection_card_path(card.collection, card), name: "card[status]", value: "drafted", method: :put, class: "btn btn--reversed" %>
+ <%= button_to "Create card", card_publish_path(card), name: "creation_type", value: "add", class: "btn" %>
+ <%= button_to "Create and add another", card_publish_path(card), method: :post, class: "btn btn--reversed", name: "creation_type", value: "add_another", form: { data: { turbo: false } } %>
+ <%= button_to "Save as a draft", collection_card_path(card.collection, card), name: "card[status]", value: "drafted", method: :put, class: "btn btn--reversed" %>
diff --git a/app/views/cards/display/_preview.html.erb b/app/views/cards/display/_preview.html.erb
index 4590b5e99..e7b8a61ec 100644
--- a/app/views/cards/display/_preview.html.erb
+++ b/app/views/cards/display/_preview.html.erb
@@ -1,7 +1,9 @@
-<% cache cacheable_preview_parts_for(card) do %>
- <%= card_article_tag card, class: "card" do %>
+<% draggable = local_assigns.fetch(:draggable, false) && card.published? %>
+
+<% cache cacheable_preview_parts_for(card, draggable) do %>
+ <%= card_article_tag card, class: "card", draggable: draggable, data: { id: card.id, drag_and_drop_target: "item" } do %>
-
+
diff --git a/app/views/cards/index.html.erb b/app/views/cards/index.html.erb
index 9cafac82d..33a2d752e 100644
--- a/app/views/cards/index.html.erb
+++ b/app/views/cards/index.html.erb
@@ -13,9 +13,20 @@
<%= render "filters/settings", filter: @filter %>
-
- <%= render "cards/index/engagement/considering", **@considering.to_h %>
- <%= render "cards/index/engagement/doing", **@doing.to_h %>
-
+<%= 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",
+ drag_and_drop_url_value: cards_drops_path(**@filter.as_params),
+ action: "
+ dragstart->drag-and-drop#dragStart
+ dragover->drag-and-drop#dragOver
+ drop->drag-and-drop#drop
+ dragend->drag-and-drop#dragEnd" } do %>
+
+ <%= render "cards/index/engagement/considering", **@considering.to_h %>
+ <%= render "cards/index/engagement/doing", **@doing.to_h %>
+
-<%= render "cards/index/engagement/closed", **@closed.to_h %>
+ <%= render "cards/index/engagement/closed", **@closed.to_h %>
+<% end %>
diff --git a/app/views/cards/index/_closure_filter.html.erb b/app/views/cards/index/_closure_filter.html.erb
index 120519c2f..7615725be 100644
--- a/app/views/cards/index/_closure_filter.html.erb
+++ b/app/views/cards/index/_closure_filter.html.erb
@@ -1,8 +1,6 @@
-
-