Merge main into collapsing-columns branch
Resolves merge conflicts by: - Integrating new Cards::Columns architecture from main - Preserving collapsible columns functionality - Maintaining stage-specific doing columns support - Using new column-based rendering while keeping expanders 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -18,6 +18,7 @@ export default class extends Controller {
|
||||
this.dialogOutlet.close()
|
||||
this.#clearTurboFrame()
|
||||
|
||||
this.searchInputTarget.querySelector("input").value = ""
|
||||
this.#showItem(this.buttonsContainerTarget)
|
||||
this.#hideItem(this.searchInputTarget)
|
||||
if (this.hasAskInputTarget) {
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
import { Controller } from "@hotwired/stimulus"
|
||||
|
||||
export default class extends Controller {
|
||||
#hiddenField
|
||||
|
||||
static targets = [ "label", "item", "hiddenFieldTemplate" ]
|
||||
static values = {
|
||||
selectPropertyName: { type: String, default: "aria-checked" },
|
||||
defaultValue: String,
|
||||
defaultLabel: String
|
||||
}
|
||||
static classes = ["withDefault"]
|
||||
|
||||
connect() {
|
||||
this.#selectedItem = this.#selectedItem
|
||||
}
|
||||
|
||||
change(event) {
|
||||
const item = event.target.closest("[role='checkbox']")
|
||||
if (item) {
|
||||
this.#selectedItem = item
|
||||
}
|
||||
}
|
||||
|
||||
get #selectedLabel() {
|
||||
const selectedValue = this.#selectedItemValue()
|
||||
|
||||
if (this.hasDefaultLabelValue && (selectedValue === this.defaultValueValue || !selectedValue)) {
|
||||
return this.defaultLabelValue
|
||||
}
|
||||
|
||||
return this.#selectedItem?.dataset?.comboboxLabel || ""
|
||||
}
|
||||
|
||||
get #selectedItem() {
|
||||
return this.itemTargets.find(item => item.getAttribute(this.selectPropertyNameValue) === "true")
|
||||
}
|
||||
|
||||
#selectedItemValue() {
|
||||
return this.#selectedItem?.dataset?.comboboxValue || ""
|
||||
}
|
||||
|
||||
set #selectedItem(item) {
|
||||
if (!item) return
|
||||
|
||||
this.#clearSelection()
|
||||
item.setAttribute(this.selectPropertyNameValue, "true")
|
||||
this.labelTarget.textContent = this.#selectedLabel
|
||||
this.hiddenField.value = item.dataset.comboboxValue
|
||||
this.hiddenField.disabled = !item.dataset.comboboxValue
|
||||
this.#updateWithDefaultClass()
|
||||
}
|
||||
|
||||
#clearSelection() {
|
||||
this.itemTargets.forEach(target => {
|
||||
target.setAttribute(this.selectPropertyNameValue, "false")
|
||||
})
|
||||
}
|
||||
|
||||
get hiddenField() {
|
||||
if (!this.#hiddenField) {
|
||||
this.#hiddenField = this.#buildHiddenField()
|
||||
}
|
||||
return this.#hiddenField
|
||||
}
|
||||
|
||||
#buildHiddenField() {
|
||||
const [field] = this.hiddenFieldTemplateTarget.content.cloneNode(true).children
|
||||
this.element.appendChild(field)
|
||||
return field
|
||||
}
|
||||
|
||||
#updateWithDefaultClass() {
|
||||
if (this.hasWithDefaultClass && this.hasDefaultValueValue) {
|
||||
const selectedValue = this.#selectedItemValue()
|
||||
const shouldHaveClass = selectedValue === this.defaultValueValue
|
||||
|
||||
if (shouldHaveClass) {
|
||||
this.element.classList.add(this.withDefaultClass)
|
||||
} else {
|
||||
this.element.classList.remove(this.withDefaultClass)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,7 @@ export default class extends Controller {
|
||||
|
||||
filter() {
|
||||
this.itemTargets.forEach(item => {
|
||||
if (filterMatches(item.innerText, this.inputTarget.value)) {
|
||||
if (filterMatches(item.textContent, this.inputTarget.value)) {
|
||||
item.removeAttribute("hidden")
|
||||
} else {
|
||||
item.toggleAttribute("hidden", true)
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import { Controller } from "@hotwired/stimulus"
|
||||
import { debounce } from "helpers/timing_helpers";
|
||||
import { post } from "@rails/request.js"
|
||||
|
||||
export default class extends Controller {
|
||||
static classes = ["filtersSet"]
|
||||
static targets = ["field", "form"]
|
||||
static values = { refreshUrl: String }
|
||||
|
||||
initialize() {
|
||||
this.debouncedToggle = debounce(this.#toggle.bind(this), 50)
|
||||
}
|
||||
|
||||
connect() {
|
||||
this.#toggle()
|
||||
}
|
||||
|
||||
change() {
|
||||
this.#toggle()
|
||||
this.#refreshSaveToggleButton()
|
||||
}
|
||||
|
||||
async fieldTargetConnected(field) {
|
||||
this.debouncedToggle()
|
||||
}
|
||||
|
||||
#toggle() {
|
||||
this.element.classList.toggle(this.filtersSetClass, this.#hasFiltersSet)
|
||||
}
|
||||
|
||||
get #hasFiltersSet() {
|
||||
return this.fieldTargets.some(field => this.#isFieldSet(field))
|
||||
}
|
||||
|
||||
#isFieldSet(field) {
|
||||
const value = field.value?.trim()
|
||||
|
||||
if (!value) return false
|
||||
|
||||
const defaultValue = this.#defaultValueForField(field)
|
||||
return defaultValue ? value !== defaultValue : true
|
||||
}
|
||||
|
||||
#defaultValueForField(field) {
|
||||
const comboboxContainer = field.closest("[data-combobox-default-value-value]")
|
||||
return comboboxContainer?.dataset?.comboboxDefaultValueValue
|
||||
}
|
||||
|
||||
#refreshSaveToggleButton() {
|
||||
post(this.refreshUrlValue, {
|
||||
body: this.#collectFilterFormData(),
|
||||
responseKind: "turbo-stream"
|
||||
})
|
||||
}
|
||||
|
||||
#collectFilterFormData() {
|
||||
const formData = new FormData()
|
||||
|
||||
this.formTargets.forEach(form => {
|
||||
const hiddenFields = form.querySelectorAll('input[type="hidden"]:not([disabled])[name]')
|
||||
hiddenFields.forEach(field => {
|
||||
formData.append(field.name, field.value)
|
||||
})
|
||||
})
|
||||
|
||||
return formData
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
import { Controller } from "@hotwired/stimulus"
|
||||
import { toSentence } from "helpers/text_helpers"
|
||||
|
||||
export default class extends Controller {
|
||||
#hiddenField
|
||||
|
||||
static targets = [ "label", "item", "hiddenFieldTemplate" ]
|
||||
static values = {
|
||||
selectPropertyName: { type: String, default: "aria-checked" },
|
||||
defaultValue: String,
|
||||
noSelectionLabel: { type: String, default: "No selection" },
|
||||
labelPrefix: String
|
||||
}
|
||||
|
||||
connect() {
|
||||
this.labelTarget.textContent = this.#selectedLabel
|
||||
this.#updateHiddenFields()
|
||||
}
|
||||
|
||||
change(event) {
|
||||
const item = event.target.closest("[role='checkbox']")
|
||||
if (item) {
|
||||
this.#toggleSelection(item)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
clear(event) {
|
||||
this.#deselectAll()
|
||||
this.#updateHiddenFields()
|
||||
this.labelTarget.textContent = this.#selectedLabel
|
||||
}
|
||||
|
||||
get #selectedLabel() {
|
||||
const selectedValues = this.#selectedValues()
|
||||
if (selectedValues.length === 0) {
|
||||
return this.noSelectionLabelValue
|
||||
}
|
||||
|
||||
const labels = this.#selectedItems().map(item => item.dataset.multiSelectionComboboxLabel)
|
||||
const sentence = toSentence(labels, {
|
||||
two_words_connector: " or ",
|
||||
last_word_connector: ", or "
|
||||
})
|
||||
|
||||
return this.hasLabelPrefixValue ? `${this.labelPrefixValue} ${sentence}` : sentence
|
||||
}
|
||||
|
||||
#toggleSelection(item) {
|
||||
const isSelected = item.getAttribute(this.selectPropertyNameValue) === "true"
|
||||
|
||||
if (isSelected) {
|
||||
item.setAttribute(this.selectPropertyNameValue, "false")
|
||||
} else {
|
||||
item.setAttribute(this.selectPropertyNameValue, "true")
|
||||
}
|
||||
|
||||
this.#updateHiddenFields()
|
||||
this.labelTarget.textContent = this.#selectedLabel
|
||||
}
|
||||
|
||||
#updateHiddenFields() {
|
||||
this.#clearHiddenFields()
|
||||
this.#addHiddenFields()
|
||||
}
|
||||
|
||||
#deselectAll() {
|
||||
this.itemTargets.forEach(item => {
|
||||
item.setAttribute(this.selectPropertyNameValue, "false")
|
||||
})
|
||||
}
|
||||
|
||||
#selectedItems() {
|
||||
return this.itemTargets.filter(item =>
|
||||
item.getAttribute(this.selectPropertyNameValue) === "true"
|
||||
)
|
||||
}
|
||||
|
||||
#selectedValues() {
|
||||
return this.#selectedItems().map(item => item.dataset.multiSelectionComboboxValue)
|
||||
}
|
||||
|
||||
#clearHiddenFields() {
|
||||
this.element.querySelectorAll('input[type="hidden"]').forEach(field => {
|
||||
if (field !== this.hiddenField) {
|
||||
field.remove()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#addHiddenFields() {
|
||||
this.#selectedValues().forEach(value => {
|
||||
const [field] = this.hiddenFieldTemplateTarget.content.cloneNode(true).children
|
||||
field.value = value
|
||||
this.element.appendChild(field)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import { Controller } from "@hotwired/stimulus"
|
||||
|
||||
export default class extends Controller {
|
||||
static values = { key: String }
|
||||
static targets = [ "input", "section" ]
|
||||
|
||||
sectionTargetConnected() {
|
||||
this.#restoreToggles()
|
||||
}
|
||||
|
||||
toggle(event) {
|
||||
const section = event.target
|
||||
if (section.hasAttribute("data-is-filtering")) return
|
||||
|
||||
const key = this.#localStorageKeyFor(section)
|
||||
if (section.open) {
|
||||
localStorage.removeItem(key)
|
||||
} else {
|
||||
localStorage.setItem(key, true)
|
||||
}
|
||||
}
|
||||
|
||||
showWhileFiltering() {
|
||||
if (this.inputTarget.value) {
|
||||
this.#expandAll();
|
||||
} else {
|
||||
this.#restoreToggles()
|
||||
}
|
||||
}
|
||||
|
||||
#expandAll() {
|
||||
this.sectionTargets.forEach(section => {
|
||||
section.setAttribute("data-is-filtering", true)
|
||||
section.open = true
|
||||
})
|
||||
}
|
||||
|
||||
#restoreToggles() {
|
||||
this.sectionTargets.forEach(section => {
|
||||
const key = this.#localStorageKeyFor(section)
|
||||
section.open = !localStorage.getItem(key)
|
||||
section.removeAttribute("data-is-filtering")
|
||||
})
|
||||
}
|
||||
|
||||
#localStorageKeyFor(section) {
|
||||
return section.getAttribute("data-nav-section-expander-key-value")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user