forked from thoughtbot/hotwire-example-template
-
Notifications
You must be signed in to change notification settings - Fork 0
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
adrianthedev
wants to merge
18
commits into
main
Choose a base branch
from
hotwire-example-kanban
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Conversation
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
No description provided.