Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Hotwire example kanban #1

Open
wants to merge 18 commits into
base: main
Choose a base branch
from
Open

Hotwire example kanban #1

wants to merge 18 commits into from

Conversation

adrianthedev
Copy link
Owner

No description provided.

seanpdoyle added 18 commits May 21, 2022 11:40
Add a variety of files for [replit.com][] support, including the
platform-specific `.replit` file, and the `replit.nix` [NixOS
configuration file][nixos].

Outside of those files, extend configuration to check for the presence
of the `REPLIT` environment variable, which will be set once the
codebase is executed on the platform.

The `config/initializers/replit.rb` initializer makes
`replit.com`-specific overrides to support serving the application from
a `replit.com`-controller subdomain, including globally disable
Cross-site Request Forgery protection.

**Do not set `REPLIT=1` in Production.**

[replit.com]: https://replit.com/
[nixos]: https://nixos.org
```sh
bin/rails generate model Board
bin/rails db:migrate
```
```sh
bin/rails generate model Stage \
        name:text \
        column_order:integer \
        board:belongs_to
bin/rails db:migrate
```
```sh
bin/rails generate model Card \
        row_order:integer \
        stage:belongs_to:index
bin/rails db:migrate
```
To set the foundation for several tables that will require arbitrary
sort ordering, depend on the [ranked-model][] gem.

[ranked-model]: https://github.com/brendon/ranked-model/tree/v0.4.7#simple-use
Pre-populate the kanban dataset with a Board with three Stages (from
left to right):

1. To-do
2. Doing
3. Done

Each Stage has Cards with rich-text content describing their task.
Using CSS and the `:first-of-type` and `:last-of-type` pseudo selectors,
hide the buttons to move the top Card "up" and the bottom Card "down".
```sh
git diff HEAD~10 :^test
```

Wrapping up:

```diff
diff --git a/app/controllers/boards_controller.rb b/app/controllers/boards_controller.rb
new file mode 100644
index 0000000..fd0396f
--- /dev/null
+++ b/app/controllers/boards_controller.rb
@@ -0,0 +1,5 @@
+class BoardsController < ApplicationController
+  def show
+    @board = Board.find params[:id]
+  end
+end
diff --git a/app/controllers/cards_controller.rb b/app/controllers/cards_controller.rb
new file mode 100644
index 0000000..e7cd02d
--- /dev/null
+++ b/app/controllers/cards_controller.rb
@@ -0,0 +1,17 @@
+class CardsController < ApplicationController
+  def update
+    @board = Board.find params[:board_id]
+    @card = @board.cards.find params[:id]
+
+    @card.update! card_params
+    @card.broadcast_changes_to_stages
+
+    redirect_to board_url(@board)
+  end
+
+  private
+
+  def card_params
+    params.require(:card).permit(:row_order_position, :stage_id)
+  end
+end
diff --git a/app/javascript/controllers/autoclick_controller.js b/app/javascript/controllers/autoclick_controller.js
new file mode 100644
index 0000000..ebc6722
--- /dev/null
+++ b/app/javascript/controllers/autoclick_controller.js
@@ -0,0 +1,7 @@
+import { Controller } from "@hotwired/stimulus"
+
+export default class extends Controller {
+  connect() {
+    this.element.click()
+  }
+}
diff --git a/app/javascript/controllers/autoremove_controller.js b/app/javascript/controllers/autoremove_controller.js
new file mode 100644
index 0000000..069e783
--- /dev/null
+++ b/app/javascript/controllers/autoremove_controller.js
@@ -0,0 +1,7 @@
+import { Controller } from "@hotwired/stimulus"
+
+export default class extends Controller {
+  connect() {
+    this.element.remove()
+  }
+}
diff --git a/app/javascript/controllers/drag_controller.js b/app/javascript/controllers/drag_controller.js
new file mode 100644
index 0000000..765ac2c
--- /dev/null
+++ b/app/javascript/controllers/drag_controller.js
@@ -0,0 +1,36 @@
+import { Controller } from "@hotwired/stimulus"
+
+export default class extends Controller {
+  static get classes() { return [ "accepting" ] }
+  static get targets() { return [ "drop", "template" ] }
+
+  start({ dataTransfer, target }) {
+    const template = this.templateTargets.find(template => target.contains(template))
+    const image = target.cloneNode(true)
+
+    dataTransfer.setData("text/html", template?.innerHTML)
+  }
+
+  accept(event) {
+    const { currentTarget, dataTransfer } = event
+
+    event.preventDefault()
+
+    dataTransfer.dropEffect = currentTarget.getAttribute("aria-dropeffect")
+    currentTarget.classList.add(...this.acceptingClasses)
+  }
+
+  reject({ currentTarget }) {
+    currentTarget.classList.remove(...this.acceptingClasses)
+  }
+
+  insert(event) {
+    const { currentTarget, dataTransfer } = event
+
+    event.preventDefault()
+
+    const dropTarget = this.dropTargets.find(dropTarget => currentTarget.contains(dropTarget))
+
+    dropTarget?.insertAdjacentHTML("beforeend", dataTransfer.getData("text/html"))
+  }
+}
diff --git a/app/javascript/controllers/scroll_controller.js b/app/javascript/controllers/scroll_controller.js
new file mode 100644
index 0000000..6d0ba73
--- /dev/null
+++ b/app/javascript/controllers/scroll_controller.js
@@ -0,0 +1,17 @@
+import { Controller } from "@hotwired/stimulus"
+
+const idsToScrollTops = {}
+
+export default class extends Controller {
+  static get targets() { return [ "container" ] }
+
+  containerTargetConnected(target) {
+    const scrollTop = idsToScrollTops[target.id]
+
+    if (scrollTop) target.scroll(0, scrollTop)
+  }
+
+  track({ target }) {
+    if (target.id) idsToScrollTops[target.id] = target.scrollTop
+  }
+}
diff --git a/app/models/board.rb b/app/models/board.rb
index 29e9728..38bf4f1 100644
--- a/app/models/board.rb
+++ b/app/models/board.rb
@@ -1,2 +1,4 @@
 class Board < ApplicationRecord
+  has_many :stages
+  has_many :cards, through: :stages
 end
diff --git a/app/models/card.rb b/app/models/card.rb
index ac6f5df..59fd763 100644
--- a/app/models/card.rb
+++ b/app/models/card.rb
@@ -6,4 +6,24 @@ class Card < ApplicationRecord
   has_rich_text :content

   ranks :row_order, with_same: :stage_id
+
+  delegate :other_stages, to: :stage
+
+  def name
+    content.to_plain_text
+  end
+
+  def broadcast_changes_to_stages
+    changed_stages.each { |stage| stage.broadcast_replace_later_to stage.board }
+  end
+
+  private
+
+  def changed_stages
+    stage.board.stages.find changed_stage_ids
+  end
+
+  def changed_stage_ids
+    saved_change_to_stage_id.presence || [ stage_id ]
+  end
 end
diff --git a/app/models/stage.rb b/app/models/stage.rb
index 612c84f..f7008e8 100644
--- a/app/models/stage.rb
+++ b/app/models/stage.rb
@@ -3,5 +3,10 @@ class Stage < ApplicationRecord

   belongs_to :board

+  has_many :cards
+  has_many :other_stages, ->(record) { without record },
+    through: :board,
+    source: :stages
+
   ranks :column_order, with_same: :board_id
 end
diff --git a/app/views/boards/show.html.erb b/app/views/boards/show.html.erb
new file mode 100644
index 0000000..ec5056a
--- /dev/null
+++ b/app/views/boards/show.html.erb
@@ -0,0 +1,7 @@
+<%= turbo_stream_from @board %>
+
+<main class="grid grid-cols-3 gap-1 h-screen">
+  <h1 class="col-span-3 h-12">Board</h1>
+
+  <%= render partial: "stages/stage", collection: @board.stages.rank(:column_order) %>
+</main>
diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb
index beaa61f..09f9cb4 100644
--- a/app/views/layouts/application.html.erb
+++ b/app/views/layouts/application.html.erb
@@ -1,5 +1,5 @@
 <!DOCTYPE html>
-<html>
+<html data-controller="scroll">
   <head>
     <title>HotwireExampleTemplate</title>
     <meta name="viewport" content="width=device-width,initial-scale=1">
diff --git a/app/views/stages/_stage.html.erb b/app/views/stages/_stage.html.erb
new file mode 100644
index 0000000..ba54f3b
--- /dev/null
+++ b/app/views/stages/_stage.html.erb
@@ -0,0 +1,64 @@
+<section id="<%= dom_id stage %>" class="row-end-auto flex flex-col overflow-y-scroll"
+    data-controller="drag"
+    data-scroll-target="container"
+    data-drag-accepting-class="opacity-25"
+    data-action="dragstart->drag#start scroll->scroll#track:passive">
+  <h2><%= stage.name %></h2>
+
+  <%= tag.ol class: "peer" do -%>
+    <% stage.cards.rank(:row_order).each do |card| %>
+      <li class="group" draggable="true" aria-dropeffect="move" data-action="dragover->drag#accept dragleave->drag#reject drop->drag#insert">
+        <template data-drag-target="template">
+          <input type="submit" formaction="<%= url_for [ stage.board, card ] %>" data-controller="autoclick autoremove" hidden>
+        </template>
+
+        <%= card.content %>
+
+        <%= form_with model: [ stage.board, card ] do |form| %>
+          <%= form.hidden_field :row_order_position, value: "up" %>
+
+          <button class="group-first-of-type:hidden">
+            Move <%= card.name %> up
+          </button>
+        <% end %>
+
+        <%= form_with model: [ stage.board, card ] do |form| %>
+          <%= form.hidden_field :row_order_position, value: "down" %>
+
+          <button class="group-last-of-type:hidden">
+            Move <%= card.name %> down
+          </button>
+        <% end %>
+
+        <%= form_with model: [ stage.board, card ] do |form| %>
+          <%= form.hidden_field :row_order_position, value: 0 %>
+
+          <%= form.label :stage_id do %>
+            Stages
+          <% end %>
+          <%= form.select :stage_id, stage.other_stages.pluck(:name, :id) %>
+          <button>
+            Move to Stage
+          </button>
+        <% end %>
+
+        <%= form_with model: [ stage.board, card ], data: { drag_target: "drop" } do |form| %>
+          <%= form.hidden_field :row_order_position, value: card.row_order_rank %>
+          <%= form.hidden_field :stage_id %>
+        <% end %>
+      </li>
+    <% end %>
+  <% end %>
+
+  <form method="post" class="hidden h-12 peer-empty:block" aria-dropeffect="move" data-drag-target="drop"
+      data-action="dragover->drag#accept dragleave->drag#reject drop->drag#insert">
+    <input type="hidden" name="_method" value="patch">
+
+    Move to <%= stage.name %>
+
+    <%= fields :card do |form| %>
+      <%= form.hidden_field :row_order_position, value: 0 %>
+      <%= form.hidden_field :stage_id, value: stage.id %>
+    <% end %>
+  </form>
+</section>
diff --git a/config/routes.rb b/config/routes.rb
index 262ffd5..82ad478 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -3,4 +3,7 @@ Rails.application.routes.draw do

   # Defines the root path route ("/")
   # root "articles#index"
+  resources :boards, only: :show do
+    resources :cards, only: :update
+  end
 end
```
When visiting the application root route (i.e. `/`), redirect to the
`boards#index` action then, within the `boards#index` action, redirect
to the first `Board` record.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants