Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions app/controllers/cards/closures_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,30 @@ class Cards::ClosuresController < ApplicationController
include CardScoped

def create
capture_card_location
@card.close
refresh_stream_if_needed

respond_to do |format|
format.turbo_stream { render_card_replacement }
format.turbo_stream
format.json { head :no_content }
end
end

def destroy
@card.reopen
refresh_stream_after_reopen

respond_to do |format|
format.turbo_stream { render_card_replacement }
format.turbo_stream
format.json { head :no_content }
end
end

private
def refresh_stream_after_reopen
if @card.awaiting_triage?
set_page_and_extract_portion_from @board.cards.awaiting_triage.latest.with_golden_first.preloaded
end
end
end
4 changes: 3 additions & 1 deletion app/controllers/cards/not_nows_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ class Cards::NotNowsController < ApplicationController
include CardScoped

def create
capture_card_location
@card.postpone
refresh_stream_if_needed

respond_to do |format|
format.turbo_stream { render_card_replacement }
format.turbo_stream
format.json { head :no_content }
end
end
Expand Down
11 changes: 11 additions & 0 deletions app/controllers/concerns/card_scoped.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,15 @@ def set_board
def render_card_replacement
render turbo_stream: turbo_stream.replace([ @card, :card_container ], partial: "cards/container", method: :morph, locals: { card: @card.reload })
end

def capture_card_location
@source_column = @card.column
@was_in_stream = @card.awaiting_triage?
end

def refresh_stream_if_needed
if @was_in_stream
set_page_and_extract_portion_from @board.cards.awaiting_triage.latest.with_golden_first.preloaded
end
end
end
2 changes: 2 additions & 0 deletions app/helpers/columns_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ def button_to_set_column(card, column)

def column_tag(id:, name:, drop_url:, collapsed: true, selected: nil, card_color: "var(--color-card-default)", data: {}, **properties, &block)
classes = token_list("cards", properties.delete(:class), "is-collapsed": collapsed)
hotkeys_disabled = data[:card_hotkeys_disabled]

data = {
drag_and_drop_target: "container",
Expand All @@ -37,6 +38,7 @@ def column_tag(id:, name:, drop_url:, collapsed: true, selected: nil, card_color
navigable_list_auto_select_value: "false",
navigable_list_actionable_items_value: "true",
navigable_list_only_act_on_focused_items_value: "true",
card_hotkeys_disabled: hotkeys_disabled,
action: "keydown->navigable-list#navigate"
}, &block)
end
Expand Down
130 changes: 130 additions & 0 deletions app/javascript/controllers/card_hotkeys_controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import { Controller } from "@hotwired/stimulus"
import { post } from "@rails/request.js"

export default class extends Controller {
static outlets = [ "navigable-list" ]

connect() {
this.morphCompletePromise = null
this.morphCompleteResolver = null
}

handleKeydown(event) {
if (this.#shouldIgnore(event)) return

const handler = this.#keyHandlers[event.key.toLowerCase()]
if (handler) {
handler.call(this, event)
}
}

// Called when turbo:morph completes - resolves our waiting promise
handleMorphComplete() {
if (this.morphCompleteResolver) {
this.morphCompleteResolver()
this.morphCompleteResolver = null
this.morphCompletePromise = null
}
}

// Private

#shouldIgnore(event) {
const target = event.target
return target.tagName === "INPUT" ||
target.tagName === "TEXTAREA" ||
target.isContentEditable ||
target.closest("input, textarea, [contenteditable], lexxy-editor")
}

get #selectedCard() {
// Find the navigable-list that currently has focus
const focusedList = this.navigableListOutlets.find(list => list.hasFocus)
if (!focusedList) return null

const currentItem = focusedList.currentItem
if (currentItem?.classList.contains("card") && !this.#hotkeysDisabled(focusedList)) {
return { card: currentItem, controller: focusedList }
}
return null
}

async #postponeCard(event) {
const selection = this.#selectedCard
if (!selection) return

const url = selection.card.dataset.cardNotNowUrl
if (url) {
event.preventDefault()
await this.#performCardAction(url, selection)
}
}

async #closeCard(event) {
const selection = this.#selectedCard
if (!selection) return

const url = selection.card.dataset.cardClosureUrl
if (url) {
event.preventDefault()
await this.#performCardAction(url, selection)
}
}

async #assignToMe(event) {
const selection = this.#selectedCard
if (!selection) return

const url = selection.card.dataset.cardAssignToMeUrl
if (url) {
event.preventDefault()
await post(url, { responseKind: "turbo-stream" })
}
}

async #performCardAction(url, selection) {
const { controller } = selection
const visibleItems = controller.visibleItems
const currentIndex = visibleItems.indexOf(selection.card)
const wasLastItem = currentIndex === visibleItems.length - 1

// Set up promise to wait for morph completion
this.morphCompletePromise = new Promise(resolve => {
this.morphCompleteResolver = resolve
})

await post(url, { responseKind: "turbo-stream" })

// Wait for Turbo Stream morph to complete
await Promise.race([
this.morphCompletePromise,
new Promise(resolve => setTimeout(resolve, 200)) // Fallback timeout
])

// Select the next card (or previous if it was the last)
const newVisibleItems = controller.visibleItems
if (newVisibleItems.length === 0) {
controller.clearSelection()
return
}

if (wasLastItem) {
controller.selectLast()
} else {
const nextIndex = Math.min(currentIndex, newVisibleItems.length - 1)
if (newVisibleItems[nextIndex]) {
await controller.selectItem(newVisibleItems[nextIndex])
}
}
}

#hotkeysDisabled(navigableList) {
return navigableList?.element.dataset.cardHotkeysDisabled === "true"
}

#keyHandlers = {
"["(event) { this.#postponeCard(event) },
"]"(event) { this.#closeCard(event) },
m(event) { this.#assignToMe(event) }
}
}
20 changes: 19 additions & 1 deletion app/javascript/controllers/navigable_list_controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@ export default class extends Controller {
this.selectItem(target, true)
}

hoverSelect({ currentTarget }) {
this.selectItem(currentTarget)
}

selectCurrentOrReset(event) {
if (this.currentItem) {
this.#setCurrentFrom(this.currentItem)
Expand Down Expand Up @@ -221,6 +225,20 @@ export default class extends Controller {
})
}

// Public accessors for card_hotkeys_controller outlet
get visibleItems() {
return this.#visibleItems
}

clearSelection() {
this.#clearSelection()
this.currentItem = null
}

get hasFocus() {
return this.element.contains(document.activeElement)
}

#keyHandlers = {
ArrowDown(event) {
if (this.supportsVerticalNavigationValue) {
Expand Down Expand Up @@ -253,6 +271,6 @@ export default class extends Controller {
} else {
this.#clickCurrentItem(event)
}
},
}
}
}
1 change: 1 addition & 0 deletions app/views/boards/show/_closed.html.erb
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<%= column_tag id: "closed-cards", name: "Done", drop_url: columns_card_drops_closure_path("__id__"), class: "cards--closed", style: "--card-color: var(--color-card-complete);",
data: {
card_hotkeys_disabled: true,
drag_and_strum_target: "container",
collapsible_columns_target: "column",
action: "focus->collapsible-columns#focusOnColumn"
Expand Down
5 changes: 4 additions & 1 deletion app/views/boards/show/_columns.html.erb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<%= tag.div class: "card-columns hide-scrollbar", data: {
controller: "collapsible-columns drag-and-drop drag-and-strum navigable-list",
controller: "collapsible-columns drag-and-drop drag-and-strum navigable-list card-hotkeys",
drag_and_drop_dragged_item_class: "drag-and-drop__dragged-item",
drag_and_drop_hover_container_class: "drag-and-drop__hover-container",
collapsible_columns_board_value: board.id,
Expand All @@ -11,8 +11,11 @@
navigable_list_prevent_handled_keys_value: true,
navigable_list_auto_select_value: false,
navigable_list_auto_scroll_value: false,
card_hotkeys_navigable_list_outlet: ".cards__transition-container",
action: "
keydown->navigable-list#navigate
keydown->card-hotkeys#handleKeydown
turbo:morph@document->card-hotkeys#handleMorphComplete
dragstart->drag-and-drop#dragStart
dragover->drag-and-drop#dragOver
dragenter->drag-and-strum#dragEnter
Expand Down
1 change: 1 addition & 0 deletions app/views/boards/show/_not_now.html.erb
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<%= column_tag id: "not-now", name: "Not Now", drop_url: columns_card_drops_not_now_path("__id__"), class: "cards--on-deck", style: "--card-color: var(--color-card-complete);",
data: {
card_hotkeys_disabled: true,
collapsible_columns_target: "column",
drag_and_strum_target: "container",
action: "focus->collapsible-columns#focusOnColumn"
Expand Down
9 changes: 9 additions & 0 deletions app/views/cards/closures/create.turbo_stream.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<%= turbo_stream.replace("closed-cards", partial: "boards/show/closed", method: :morph, locals: { board: @card.board }) %>

<% if @source_column %>
<%= turbo_stream.replace(dom_id(@source_column), partial: "boards/show/column", method: :morph, locals: { column: @source_column }) %>
<% elsif @was_in_stream %>
<%= turbo_stream.replace("the-stream", partial: "boards/show/stream", method: :morph, locals: { board: @card.board, page: @page }) %>
<% end %>

<%= turbo_stream.replace([ @card, :card_container ], partial: "cards/container", method: :morph, locals: { card: @card.reload }) %>
9 changes: 9 additions & 0 deletions app/views/cards/closures/destroy.turbo_stream.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<%= turbo_stream.replace("closed-cards", partial: "boards/show/closed", method: :morph, locals: { board: @card.board }) %>

<% if @card.column %>
<%= turbo_stream.replace(dom_id(@card.column), partial: "boards/show/column", method: :morph, locals: { column: @card.column }) %>
<% elsif @card.awaiting_triage? %>
<%= turbo_stream.replace("the-stream", partial: "boards/show/stream", method: :morph, locals: { board: @card.board, page: @page }) %>
<% end %>

<%= turbo_stream.replace([ @card, :card_container ], partial: "cards/container", method: :morph, locals: { card: @card.reload }) %>
18 changes: 17 additions & 1 deletion app/views/cards/display/_preview.html.erb
Original file line number Diff line number Diff line change
@@ -1,6 +1,22 @@
<% draggable = local_assigns.fetch(:draggable, false) && card.published? %>

<%= card_article_tag card, class: "card", draggable: draggable, data: { id: card.number, drag_and_drop_target: "item", navigable_list_target: "item", css_variable_counter_target: "item" }, tabindex: 0 do %>
<% card_data = {
id: card.number,
drag_and_drop_target: "item",
navigable_list_target: "item",
css_variable_counter_target: "item"
} %>

<% if card.open? %>
<% card_data[:card_not_now_url] = card_not_now_path(card) %>
<% card_data[:card_closure_url] = card_closure_path(card) %>
<% card_data[:action] = "mouseenter->navigable-list#hoverSelect" %>
<% if Current.user %>
<% card_data[:card_assign_to_me_url] = card_assignments_path(card, params: { assignee_id: Current.user.id }) %>
<% end %>
<% end %>

<%= card_article_tag card, class: "card", draggable: draggable, data: card_data, tabindex: 0 do %>
<div class="flex flex-column flex-item-grow max-inline-size">
<%= link_to card_path(card), draggable: false, class: "card__link", title: card_title_tag(card), data: { action: "dialog#close", turbo_frame: "_top" } do %>
<span class="for-screen-reader"><%= card.title %></span>
Expand Down
2 changes: 1 addition & 1 deletion app/views/cards/display/common/_assignees.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
</span>
</button>

<% if Current.user %>
<% if Current.user && !local_assigns[:preview] %>
<%= button_to "Assign to me", card_assignments_path(card, params: { assignee_id: Current.user.id }), method: :post, data: {controller: "hotkey", action: "keydown.m@document->hotkey#click" }, hidden: true %>
<% end %>

Expand Down
9 changes: 9 additions & 0 deletions app/views/cards/not_nows/create.turbo_stream.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<%= turbo_stream.replace("not-now", partial: "boards/show/not_now", method: :morph, locals: { board: @card.board }) %>

<% if @source_column %>
<%= turbo_stream.replace(dom_id(@source_column), partial: "boards/show/column", method: :morph, locals: { column: @source_column }) %>
<% elsif @was_in_stream %>
<%= turbo_stream.replace("the-stream", partial: "boards/show/stream", method: :morph, locals: { board: @card.board, page: @page }) %>
<% end %>

<%= turbo_stream.replace([ @card, :card_container ], partial: "cards/container", method: :morph, locals: { card: @card.reload }) %>