forked from fixin.me/fixin.me
Measurement form based on select-styled <details>
This commit is contained in:
1
app/assets/images/pictograms/chevron-down.svg
Normal file
1
app/assets/images/pictograms/chevron-down.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" id="icon" viewBox="0 0 24 24"><path d="M7.41,8.58L12,13.17L16.59,8.58L18,10L12,16L6,10L7.41,8.58Z" /></svg>
|
||||||
|
After Width: | Height: | Size: 148 B |
1
app/assets/images/pictograms/pencil-outline.svg
Normal file
1
app/assets/images/pictograms/pencil-outline.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" id="icon" viewBox="0 0 24 24"><path d="M14.06,9L15,9.94L5.92,19H5V18.08L14.06,9M17.66,3C17.41,3 17.15,3.1 16.96,3.29L15.13,5.12L18.88,8.87L20.71,7.04C21.1,6.65 21.1,6 20.71,5.63L18.37,3.29C18.17,3.09 17.92,3 17.66,3M14.06,6.19L3,17.25V21H6.75L17.81,9.94L14.06,6.19Z" /></svg>
|
||||||
|
After Width: | Height: | Size: 316 B |
@@ -49,6 +49,7 @@
|
|||||||
|
|
||||||
|
|
||||||
/* TODO: collapse gaps around empty rows (`topside`) once possible
|
/* TODO: collapse gaps around empty rows (`topside`) once possible
|
||||||
|
* with grid-collapse property and remove alternative grid-template
|
||||||
* https://github.com/w3c/csswg-drafts/issues/5813 */
|
* https://github.com/w3c/csswg-drafts/issues/5813 */
|
||||||
body {
|
body {
|
||||||
display: grid;
|
display: grid;
|
||||||
@@ -56,20 +57,27 @@ body {
|
|||||||
grid-template-areas:
|
grid-template-areas:
|
||||||
"header header header"
|
"header header header"
|
||||||
"nav nav nav"
|
"nav nav nav"
|
||||||
"leftempty topside rightempty"
|
"leftside topside rightside"
|
||||||
"leftside main rightside";
|
"leftside main rightside";
|
||||||
grid-template-columns: 1fr auto 1fr;
|
grid-template-columns: 1fr minmax(max-content, 2fr) 1fr;
|
||||||
grid-template-rows: repeat(4, auto);
|
|
||||||
font-family: system-ui;
|
font-family: system-ui;
|
||||||
margin: 0.4em;
|
margin: 0.4em;
|
||||||
}
|
}
|
||||||
|
body:not(:has(.topside)) {
|
||||||
|
grid-template-areas:
|
||||||
|
"header header header"
|
||||||
|
"nav nav nav"
|
||||||
|
"leftside main rightside";
|
||||||
|
}
|
||||||
button,
|
button,
|
||||||
|
details,
|
||||||
input,
|
input,
|
||||||
select,
|
select,
|
||||||
textarea {
|
textarea {
|
||||||
background-color: inherit;
|
background-color: inherit;
|
||||||
font: inherit;
|
font: inherit;
|
||||||
}
|
}
|
||||||
|
details,
|
||||||
input,
|
input,
|
||||||
select {
|
select {
|
||||||
text-align: inherit;
|
text-align: inherit;
|
||||||
@@ -86,9 +94,10 @@ input[type=submit] {
|
|||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
/* [hidden] submit controls cannot have `display` set as it makes them visible */
|
||||||
.button,
|
.button,
|
||||||
button,
|
button:not([hidden]),
|
||||||
input[type=submit],
|
input[type=submit]:not([hidden]),
|
||||||
.tab {
|
.tab {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
color: var(--color-gray);
|
color: var(--color-gray);
|
||||||
@@ -105,11 +114,13 @@ input[type=submit] {
|
|||||||
}
|
}
|
||||||
input:not([type=submit]):not([type=checkbox]),
|
input:not([type=submit]):not([type=checkbox]),
|
||||||
select,
|
select,
|
||||||
|
summary,
|
||||||
textarea {
|
textarea {
|
||||||
padding: 0.2em 0.4em;
|
padding: 0.2em 0.4em;
|
||||||
}
|
}
|
||||||
.button,
|
.button,
|
||||||
button,
|
button,
|
||||||
|
details,
|
||||||
input,
|
input,
|
||||||
select,
|
select,
|
||||||
textarea {
|
textarea {
|
||||||
@@ -134,6 +145,9 @@ button > svg:not(:last-child) {
|
|||||||
fieldset {
|
fieldset {
|
||||||
padding: 0.4em;
|
padding: 0.4em;
|
||||||
}
|
}
|
||||||
|
fieldset:not(:has(input, select, textarea)) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
legend {
|
legend {
|
||||||
color: var(--color-gray);
|
color: var(--color-gray);
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -191,6 +205,7 @@ input::-webkit-outer-spin-button {
|
|||||||
-webkit-appearance: none;
|
-webkit-appearance: none;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
details:hover,
|
||||||
input:hover,
|
input:hover,
|
||||||
select:hover,
|
select:hover,
|
||||||
textarea:hover {
|
textarea:hover {
|
||||||
@@ -203,9 +218,11 @@ textarea:invalid {
|
|||||||
border-color: var(--color-red);
|
border-color: var(--color-red);
|
||||||
outline: solid 1px var(--color-red);
|
outline: solid 1px var(--color-red);
|
||||||
}
|
}
|
||||||
|
details:hover,
|
||||||
select:hover {
|
select:hover {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
details:focus-visible,
|
||||||
input:focus-visible,
|
input:focus-visible,
|
||||||
select:focus-within,
|
select:focus-within,
|
||||||
select:focus-visible,
|
select:focus-visible,
|
||||||
@@ -417,10 +434,10 @@ table.items td.link a::after {
|
|||||||
table.items td:first-child {
|
table.items td:first-child {
|
||||||
padding-inline-start: calc(1em + var(--depth) * 0.8em);
|
padding-inline-start: calc(1em + var(--depth) * 0.8em);
|
||||||
}
|
}
|
||||||
table.items td:has(input, textarea) {
|
table.items td:has(input, select, textarea) {
|
||||||
padding-inline-start: calc(0.6em - 0.9px);
|
padding-inline-start: calc(0.6em - 0.9px);
|
||||||
}
|
}
|
||||||
table.items td:first-child:has(input, textarea) {
|
table.items td:first-child:has(input, select, textarea) {
|
||||||
padding-inline-start: calc(0.6em + var(--depth) * 0.8em - 0.9px);
|
padding-inline-start: calc(0.6em + var(--depth) * 0.8em - 0.9px);
|
||||||
}
|
}
|
||||||
table.items th:last-child {
|
table.items th:last-child {
|
||||||
@@ -484,12 +501,6 @@ table.items td:not(:first-child),
|
|||||||
color: var(--color-table-gray);
|
color: var(--color-table-gray);
|
||||||
fill: var(--color-table-gray);
|
fill: var(--color-table-gray);
|
||||||
}
|
}
|
||||||
table.items td.hint {
|
|
||||||
color: var(--color-table-gray);
|
|
||||||
font-style: italic;
|
|
||||||
font-size: 0.8rem;
|
|
||||||
padding: 1em;
|
|
||||||
}
|
|
||||||
table.items svg {
|
table.items svg {
|
||||||
height: 1.2rem;
|
height: 1.2rem;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
@@ -555,16 +566,34 @@ form table.items td:first-child {
|
|||||||
.extendedright {
|
.extendedright {
|
||||||
margin-right: auto;
|
margin-right: auto;
|
||||||
}
|
}
|
||||||
|
.hexpand {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
.hflex {
|
.hflex {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 0.8em;
|
gap: 0.8em;
|
||||||
}
|
}
|
||||||
|
.hflex.reverse {
|
||||||
|
flex-direction: row-reverse;
|
||||||
|
}
|
||||||
|
.hflex.centered {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.hint {
|
||||||
|
color: var(--color-table-gray);
|
||||||
|
font-style: italic;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
.vflex {
|
.vflex {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 0.8em;
|
gap: 0.8em;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
[disabled] {
|
[disabled] {
|
||||||
|
/* label:has(input[disabled]) {
|
||||||
|
* TODO: disabled checkbox blue square focus removal; disabled label styling
|
||||||
|
* */
|
||||||
border-color: var(--color-border-gray) !important;
|
border-color: var(--color-border-gray) !important;
|
||||||
color: var(--color-border-gray) !important;
|
color: var(--color-border-gray) !important;
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
@@ -574,3 +603,71 @@ form table.items td:first-child {
|
|||||||
.unwrappable {
|
.unwrappable {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
details {
|
||||||
|
align-content: center;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
summary {
|
||||||
|
align-items: center;
|
||||||
|
color: var(--color-gray);
|
||||||
|
display: flex;
|
||||||
|
gap: 0.2em;
|
||||||
|
height: 100%;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
summary::before {
|
||||||
|
background-color: #000;
|
||||||
|
content: "";
|
||||||
|
height: 1em;
|
||||||
|
mask-image: url('pictograms/chevron-down.svg');
|
||||||
|
mask-size: cover;
|
||||||
|
width: 1em;
|
||||||
|
}
|
||||||
|
summary:has(.button) {
|
||||||
|
padding-block: 0;
|
||||||
|
padding-inline-end: 0;
|
||||||
|
}
|
||||||
|
summary .button {
|
||||||
|
border: 1px var(--color-border-gray);
|
||||||
|
border-radius: 0;
|
||||||
|
border-style: none none none solid;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
summary span {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
details[open] summary::before {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
summary::marker {
|
||||||
|
padding-left: 0.25em;
|
||||||
|
}
|
||||||
|
/* TODO: use ::details-content ? */
|
||||||
|
details[open] ul {
|
||||||
|
background: white;
|
||||||
|
border: solid 1px var(--color-border-gray);
|
||||||
|
border-radius: 0.25em;
|
||||||
|
box-sizing: content-box;
|
||||||
|
box-shadow: 1px 1px 3px var(--color-border-gray);
|
||||||
|
margin: 0;
|
||||||
|
margin-left: -0.9px;
|
||||||
|
padding-left: 0;
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
li:hover {
|
||||||
|
background-color: var(--color-focus-gray);
|
||||||
|
}
|
||||||
|
li label {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
line-height: 1.4em;
|
||||||
|
}
|
||||||
|
li input[type=checkbox] {
|
||||||
|
margin: 0 0.25em;
|
||||||
|
}
|
||||||
|
li::marker {
|
||||||
|
content: '';
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,36 +1,11 @@
|
|||||||
class MeasurementsController < ApplicationController
|
class MeasurementsController < ApplicationController
|
||||||
before_action :find_quantity, only: [:new, :discard]
|
|
||||||
before_action :find_prev_quantities, only: [:new, :discard]
|
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@quantities = current_user.quantities.ordered
|
@measurements = []
|
||||||
|
#@measurements = current_user.units.ordered.includes(:base, :subunits)
|
||||||
end
|
end
|
||||||
|
|
||||||
def new
|
def new
|
||||||
quantities =
|
@quantities = current_user.quantities.ordered
|
||||||
case params[:scope]
|
|
||||||
when 'children'
|
|
||||||
@quantity.subquantities
|
|
||||||
when 'subtree'
|
|
||||||
@quantity.progenies
|
|
||||||
else
|
|
||||||
[@quantity]
|
|
||||||
end
|
|
||||||
quantities -= @prev_quantities
|
|
||||||
@readouts = current_user.readouts.build(quantities.map { |q| {quantity: q} })
|
|
||||||
|
|
||||||
@units = current_user.units.ordered
|
|
||||||
|
|
||||||
all_quantities = @prev_quantities + quantities
|
|
||||||
@common_ancestor = current_user.quantities
|
|
||||||
.common_ancestors(all_quantities.map(&:parent_id)).first
|
|
||||||
end
|
|
||||||
|
|
||||||
def discard
|
|
||||||
@prev_quantities -= [@quantity]
|
|
||||||
|
|
||||||
@common_ancestor = current_user.quantities
|
|
||||||
.common_ancestors(@prev_quantities.map(&:parent_id)).first
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
@@ -38,15 +13,4 @@ class MeasurementsController < ApplicationController
|
|||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def find_quantity
|
|
||||||
@quantity = current_user.quantities.find_by!(id: params[:id])
|
|
||||||
end
|
|
||||||
|
|
||||||
def find_prev_quantities
|
|
||||||
prev_quantity_ids = params[:readouts]&.map { |r| r[:quantity_id] } || []
|
|
||||||
@prev_quantities = current_user.quantities.find(prev_quantity_ids)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|||||||
39
app/controllers/readouts_controller.rb
Normal file
39
app/controllers/readouts_controller.rb
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
class ReadoutsController < ApplicationController
|
||||||
|
before_action :find_quantities, only: [:new]
|
||||||
|
before_action :find_quantity, only: [:discard]
|
||||||
|
before_action :find_prev_quantities, only: [:new, :discard]
|
||||||
|
|
||||||
|
def new
|
||||||
|
@quantities -= @prev_quantities
|
||||||
|
# TODO: raise ParameterInvalid if new_quantities.empty?
|
||||||
|
@readouts = current_user.readouts.build(@quantities.map { |q| {quantity: q} })
|
||||||
|
|
||||||
|
@user_units = current_user.units.ordered
|
||||||
|
|
||||||
|
quantities = @prev_quantities + @quantities
|
||||||
|
@superquantity = current_user.quantities
|
||||||
|
.common_ancestors(quantities.map(&:parent_id)).first
|
||||||
|
end
|
||||||
|
|
||||||
|
def discard
|
||||||
|
@prev_quantities -= [@quantity]
|
||||||
|
|
||||||
|
@superquantity = current_user.quantities
|
||||||
|
.common_ancestors(@prev_quantities.map(&:parent_id)).first
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def find_quantities
|
||||||
|
@quantities = current_user.quantities.find(params[:quantity])
|
||||||
|
end
|
||||||
|
|
||||||
|
def find_quantity
|
||||||
|
@quantity = current_user.quantities.find_by!(id: params[:id])
|
||||||
|
end
|
||||||
|
|
||||||
|
def find_prev_quantities
|
||||||
|
prev_quantity_ids = params[:readouts]&.map { |r| r[:quantity_id] } || []
|
||||||
|
@prev_quantities = current_user.quantities.find(prev_quantity_ids)
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -75,12 +75,11 @@ module ApplicationHelper
|
|||||||
end
|
end
|
||||||
|
|
||||||
def number_field(method, options = {})
|
def number_field(method, options = {})
|
||||||
value = object.public_send(method)
|
attr_type = object.type_for_attribute(method)
|
||||||
if value.is_a?(BigDecimal)
|
if attr_type.type == :decimal
|
||||||
options[:value] = value.to_scientific
|
options[:value] = object.public_send(method)&.to_scientific
|
||||||
type = object.class.type_for_attribute(method)
|
options[:step] ||= BigDecimal(10).power(-attr_type.scale)
|
||||||
options[:step] ||= BigDecimal(10).power(-type.scale)
|
options[:max] ||= BigDecimal(10).power(attr_type.precision - attr_type.scale) -
|
||||||
options[:max] ||= BigDecimal(10).power(type.precision - type.scale) -
|
|
||||||
options[:step]
|
options[:step]
|
||||||
options[:min] = options[:min] == :step ? options[:step] : options[:min]
|
options[:min] = options[:min] == :step ? options[:step] : options[:min]
|
||||||
options[:min] ||= -options[:max]
|
options[:min] ||= -options[:max]
|
||||||
|
|||||||
@@ -1,2 +1,11 @@
|
|||||||
module QuantitiesHelper
|
module QuantitiesHelper
|
||||||
|
def quantities_check_boxes
|
||||||
|
# Closing <details> on focusout event depends on relatedTarget for internal
|
||||||
|
# actions being non-null. To ensure this, all top-layer elements of
|
||||||
|
# ::details-content must accept focus, e.g. <label> needs tabindex="-1" */
|
||||||
|
collection_check_boxes(nil, :quantity, @quantities, :id, :to_s_with_depth,
|
||||||
|
include_hidden: false) do |b|
|
||||||
|
content_tag :li, b.label(tabindex: -1) { b.check_box + b.text }
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -9,31 +9,70 @@ function showPage(event) {
|
|||||||
}
|
}
|
||||||
document.addEventListener('turbo:load', showPage)
|
document.addEventListener('turbo:load', showPage)
|
||||||
|
|
||||||
|
function detailsChange(event) {
|
||||||
|
var target = event.currentTarget
|
||||||
|
var count = target.querySelectorAll('input:checked:not([disabled])').length
|
||||||
|
var span = target.querySelector('summary > span')
|
||||||
|
var button = target.querySelector('button')
|
||||||
|
if (count > 0) {
|
||||||
|
span.textContent = count + ' selected';
|
||||||
|
Turbo.StreamElement.prototype.enableElement(button)
|
||||||
|
} else {
|
||||||
|
span.textContent = span.getAttribute('data-prompt')
|
||||||
|
Turbo.StreamElement.prototype.disableElement(button)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
window.detailsChange = detailsChange
|
||||||
|
|
||||||
|
/* Close open <details> when focus lost */
|
||||||
|
function detailsClose(event) {
|
||||||
|
if (!event.relatedTarget ||
|
||||||
|
event.relatedTarget.closest("details") != event.currentTarget) {
|
||||||
|
event.currentTarget.removeAttribute("open")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
window.detailsClose = detailsClose
|
||||||
|
|
||||||
|
window.detailsObserver = new MutationObserver((mutations) => {
|
||||||
|
mutations[0].target.dispatchEvent(new Event('change', {bubbles: true}))
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
/* Turbo stream actions */
|
/* Turbo stream actions */
|
||||||
|
Turbo.StreamElement.prototype.disableElement = function(element) {
|
||||||
|
element.setAttribute("disabled", "disabled")
|
||||||
|
element.setAttribute("aria-disabled", "true")
|
||||||
|
element.setAttribute("tabindex", "-1")
|
||||||
|
}
|
||||||
|
Turbo.StreamActions.disable = function() {
|
||||||
|
this.targetElements.forEach((e) => { this.disableElement(e) })
|
||||||
|
}
|
||||||
|
|
||||||
Turbo.StreamElement.prototype.enableElement = function(element) {
|
Turbo.StreamElement.prototype.enableElement = function(element) {
|
||||||
element.removeAttribute("disabled")
|
element.removeAttribute("disabled")
|
||||||
element.removeAttribute("aria-disabled")
|
element.removeAttribute("aria-disabled")
|
||||||
// 'tabindex' is not used explicitly, so removing it is safe
|
// Assume 'tabindex' is not used explicitly, so removing it is safe
|
||||||
element.removeAttribute("tabindex")
|
element.removeAttribute("tabindex")
|
||||||
}
|
}
|
||||||
|
|
||||||
Turbo.StreamActions.disable = function() {
|
|
||||||
this.targetElements.forEach((e) => {
|
|
||||||
e.setAttribute("disabled", "disabled")
|
|
||||||
e.setAttribute("aria-disabled", "true")
|
|
||||||
e.setAttribute("tabindex", "-1")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
Turbo.StreamActions.enable = function() {
|
Turbo.StreamActions.enable = function() {
|
||||||
this.targetElements.forEach((e) => { this.enableElement(e) })
|
this.targetElements.forEach((e) => { this.enableElement(e) })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* TODO: change to visibility = collapse to avoid width change? */
|
||||||
Turbo.StreamActions.hide = function() {
|
Turbo.StreamActions.hide = function() {
|
||||||
this.targetElements.forEach((e) => { e.style.display = "none" })
|
this.targetElements.forEach((e) => { e.style.display = "none" })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Turbo.StreamActions.show = function() {
|
||||||
|
this.targetElements.forEach((e) => { e.style.removeProperty("display") })
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Turbo.StreamActions.collapse = function() {
|
||||||
|
this.targetElements.forEach((e) => { e.style.visibility = "collapse" })
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
Turbo.StreamActions.close_form = function() {
|
Turbo.StreamActions.close_form = function() {
|
||||||
this.targetElements.forEach((e) => {
|
this.targetElements.forEach((e) => {
|
||||||
/* Move focus if there's no focus or focus inside form being closed */
|
/* Move focus if there's no focus or focus inside form being closed */
|
||||||
@@ -54,28 +93,62 @@ Turbo.StreamActions.close_form = function() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Turbo.StreamActions.unselect = function() {
|
||||||
|
this.targetElements.forEach((e) => {
|
||||||
|
e.checked = false
|
||||||
|
this.enableElement(e)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/* Items table drag and drop support */
|
function formProcessKey(event) {
|
||||||
function processKey(event) {
|
switch (event.key) {
|
||||||
if (event.key == "Escape") {
|
case "Escape":
|
||||||
event.currentTarget.querySelector("a[name=cancel]").click();
|
event.currentTarget.querySelector("a[name=cancel]").click()
|
||||||
|
break
|
||||||
|
case "Enter":
|
||||||
|
event.currentTarget.querySelector("button[name=button]").click()
|
||||||
|
event.preventDefault()
|
||||||
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
window.processKey = processKey;
|
window.formProcessKey = formProcessKey
|
||||||
|
|
||||||
var lastEnterTime;
|
function detailsProcessKey(event) {
|
||||||
function dragStart(event) {
|
switch (event.key) {
|
||||||
lastEnterTime = event.timeStamp;
|
case "Escape":
|
||||||
var row = event.currentTarget;
|
if (event.currentTarget.hasAttribute("open")) {
|
||||||
row.closest("table").querySelectorAll("thead tr").forEach((tr) => {
|
event.currentTarget.removeAttribute("open")
|
||||||
tr.toggleAttribute("hidden");
|
event.stopPropagation()
|
||||||
});
|
}
|
||||||
event.dataTransfer.setData("text/plain", row.getAttribute("data-drag-path"));
|
break
|
||||||
var rowRectangle = row.getBoundingClientRect();
|
case "Enter":
|
||||||
event.dataTransfer.setDragImage(row, event.x - rowRectangle.left, event.y - rowRectangle.top);
|
var button = event.currentTarget.querySelector("button:not([disabled])")
|
||||||
event.dataTransfer.dropEffect = "move";
|
if (button) {
|
||||||
|
button.click()
|
||||||
|
// Autofocus won't be respected unless target is blurred
|
||||||
|
event.target.blur()
|
||||||
|
event.preventDefault()
|
||||||
|
event.stopPropagation()
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
window.dragStart = dragStart;
|
window.detailsProcessKey = detailsProcessKey;
|
||||||
|
|
||||||
|
/* Items table drag and drop support */
|
||||||
|
var lastEnterTime
|
||||||
|
function dragStart(event) {
|
||||||
|
lastEnterTime = event.timeStamp
|
||||||
|
var row = event.currentTarget
|
||||||
|
row.closest("table").querySelectorAll("thead tr").forEach((tr) => {
|
||||||
|
tr.toggleAttribute("hidden")
|
||||||
|
})
|
||||||
|
event.dataTransfer.setData("text/plain", row.getAttribute("data-drag-path"))
|
||||||
|
var rowRectangle = row.getBoundingClientRect()
|
||||||
|
event.dataTransfer.setDragImage(row, event.x - rowRectangle.left, event.y - rowRectangle.top)
|
||||||
|
event.dataTransfer.dropEffect = "move"
|
||||||
|
}
|
||||||
|
window.dragStart = dragStart
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Drag tracking assumptions (based on FF 122.0 experience):
|
* Drag tracking assumptions (based on FF 122.0 experience):
|
||||||
@@ -87,44 +160,44 @@ window.dragStart = dragStart;
|
|||||||
* and outside. This should probably be fixed in browser, than patched here.
|
* and outside. This should probably be fixed in browser, than patched here.
|
||||||
*/
|
*/
|
||||||
function dragEnter(event) {
|
function dragEnter(event) {
|
||||||
//console.log(event.timeStamp + " " + event.type + ": " + event.currentTarget.id);
|
//console.log(event.timeStamp + " " + event.type + ": " + event.currentTarget.id)
|
||||||
dragLeave(event);
|
dragLeave(event)
|
||||||
lastEnterTime = event.timeStamp;
|
lastEnterTime = event.timeStamp
|
||||||
const id = event.currentTarget.getAttribute("data-drop-id");
|
const id = event.currentTarget.getAttribute("data-drop-id")
|
||||||
document.getElementById(id).classList.add("dropzone");
|
document.getElementById(id).classList.add("dropzone")
|
||||||
}
|
}
|
||||||
window.dragEnter = dragEnter;
|
window.dragEnter = dragEnter
|
||||||
|
|
||||||
function dragOver(event) {
|
function dragOver(event) {
|
||||||
event.preventDefault();
|
event.preventDefault()
|
||||||
}
|
}
|
||||||
window.dragOver = dragOver;
|
window.dragOver = dragOver
|
||||||
|
|
||||||
function dragLeave(event) {
|
function dragLeave(event) {
|
||||||
//console.log(event.timeStamp + " " + event.type + ": " + event.currentTarget.id);
|
//console.log(event.timeStamp + " " + event.type + ": " + event.currentTarget.id)
|
||||||
// Leave has been accounted for by Enter at the same timestamp, processed earlier
|
// Leave has been accounted for by Enter at the same timestamp, processed earlier
|
||||||
if (event.timeStamp <= lastEnterTime) return;
|
if (event.timeStamp <= lastEnterTime) return
|
||||||
event.currentTarget.closest("table").querySelectorAll(".dropzone").forEach((tr) => {
|
event.currentTarget.closest("table").querySelectorAll(".dropzone").forEach((tr) => {
|
||||||
tr.classList.remove("dropzone");
|
tr.classList.remove("dropzone")
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
window.dragLeave = dragLeave;
|
window.dragLeave = dragLeave
|
||||||
|
|
||||||
function dragEnd(event) {
|
function dragEnd(event) {
|
||||||
dragLeave(event);
|
dragLeave(event)
|
||||||
event.currentTarget.closest("table").querySelectorAll("thead tr").forEach((tr) => {
|
event.currentTarget.closest("table").querySelectorAll("thead tr").forEach((tr) => {
|
||||||
tr.toggleAttribute("hidden");
|
tr.toggleAttribute("hidden")
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
window.dragEnd = dragEnd;
|
window.dragEnd = dragEnd
|
||||||
|
|
||||||
function drop(event) {
|
function drop(event) {
|
||||||
event.preventDefault();
|
event.preventDefault()
|
||||||
|
|
||||||
var params = new URLSearchParams();
|
var params = new URLSearchParams()
|
||||||
var id_param = event.currentTarget.getAttribute("data-drop-id-param");
|
var id_param = event.currentTarget.getAttribute("data-drop-id-param")
|
||||||
var id = event.currentTarget.getAttribute("data-drop-id").split("_").pop();
|
var id = event.currentTarget.getAttribute("data-drop-id").split("_").pop()
|
||||||
params.append(id_param, id);
|
params.append(id_param, id)
|
||||||
|
|
||||||
fetch(event.dataTransfer.getData("text/plain"), {
|
fetch(event.dataTransfer.getData("text/plain"), {
|
||||||
body: params,
|
body: params,
|
||||||
@@ -138,4 +211,4 @@ function drop(event) {
|
|||||||
.then(response => response.text())
|
.then(response => response.text())
|
||||||
.then(html => Turbo.renderStreamMessage(html))
|
.then(html => Turbo.renderStreamMessage(html))
|
||||||
}
|
}
|
||||||
window.drop = drop;
|
window.drop = drop
|
||||||
|
|||||||
3
app/models/measurement.rb
Normal file
3
app/models/measurement.rb
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
class Measurement
|
||||||
|
include ActiveModel::Model
|
||||||
|
end
|
||||||
@@ -100,6 +100,11 @@ class Quantity < ApplicationRecord
|
|||||||
name
|
name
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def to_s_with_depth
|
||||||
|
# em space, U+2003
|
||||||
|
' ' * depth + name
|
||||||
|
end
|
||||||
|
|
||||||
def destroyable?
|
def destroyable?
|
||||||
subquantities.empty?
|
subquantities.empty?
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,25 +1,37 @@
|
|||||||
<% @readouts.each do |readout| %>
|
<%= tabular_form_with model: Measurement.new, id: :measurement_form,
|
||||||
<%= tabular_fields_for 'readouts[]', readout do |form| %>
|
class: 'topside vflex', html: {onkeydown: 'formProcessKey(event)'} do |form| %>
|
||||||
<%- tag.tr id: dom_id(readout.quantity, :new, :readout),
|
<fieldset>
|
||||||
onkeydown: 'processKey(event)' do %>
|
<table class="items centered">
|
||||||
<%= tag.td id: dom_id(readout.quantity, nil, :pathname) do %>
|
<tbody id="readouts"></tbody>
|
||||||
<%= readout.quantity.relative_pathname(@common_ancestor) %>
|
</table>
|
||||||
<%end%>
|
</fieldset>
|
||||||
<td>
|
|
||||||
<%= form.number_field :value, required: true, autofocus: true, size: 10 %>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<%= form.select :unit_id, options_from_collection_for_select(
|
|
||||||
@units, :id, ->(u){ sanitize(' '*(u.base_id ? 1 : 0) + u.symbol) }
|
|
||||||
) %>
|
|
||||||
</td>
|
|
||||||
|
|
||||||
<td class="actions">
|
<div class="hflex">
|
||||||
<%= image_button_tag '', 'delete-outline', class: 'dangerous',
|
<details id="quantity_select" class="hexpand" open
|
||||||
formaction: discard_new_measurement_path(readout.quantity),
|
onkeydown="detailsProcessKey(event)">
|
||||||
formmethod: :get, formnovalidate: true, data: {turbo_stream: true} %>
|
<summary autofocus>
|
||||||
<%= form.hidden_field :quantity_id %>
|
<!-- TODO: Set content with CSS when span empty to avoid duplication -->
|
||||||
</td>
|
<span data-prompt="<%= t('.select_quantity') %>">
|
||||||
<% end %>
|
<%= t('.select_quantity') %>
|
||||||
<% end %>
|
</span>
|
||||||
|
<%= image_button_tag t(:apply), "update", name: nil, disabled: true,
|
||||||
|
formaction: new_readout_path, formmethod: :get, formnovalidate: true,
|
||||||
|
data: {turbo_stream: true} %>
|
||||||
|
</summary>
|
||||||
|
<ul><%= quantities_check_boxes %></ul>
|
||||||
|
</details>
|
||||||
|
<%= form.button id: :create_measurement_button, disabled: true -%>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="hflex reverse">
|
||||||
|
<%= image_link_to t(:cancel), "close-outline", measurements_path, name: :cancel,
|
||||||
|
class: 'dangerous', onclick: render_turbo_stream('form_close') %>
|
||||||
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
quantity_select.addEventListener('focusout', detailsClose)
|
||||||
|
quantity_select.addEventListener('change', detailsChange)
|
||||||
|
detailsObserver.observe(quantity_select.querySelector('ul'),
|
||||||
|
{subtree: true, attributeFilter: ['disabled']})
|
||||||
|
</script>
|
||||||
|
|||||||
@@ -1,2 +1,4 @@
|
|||||||
<%= turbo_stream.update :new_readouts_form %>
|
|
||||||
<%= turbo_stream.update :flashes %>
|
<%= turbo_stream.update :flashes %>
|
||||||
|
<%= turbo_stream.remove :measurement_form %>
|
||||||
|
<%= turbo_stream.show :no_items -%>
|
||||||
|
<%= turbo_stream.enable :new_measurement_link -%>
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
<%= tabular_fields_for Readout.new do |form| %>
|
|
||||||
<fieldset>
|
|
||||||
<legend>
|
|
||||||
<%= tag.span id: :new_readouts_form_legend %>
|
|
||||||
<%= image_link_to '', "close-outline", measurements_path, name: :cancel,
|
|
||||||
class: 'dangerous', onclick: render_turbo_stream('form_close') %>
|
|
||||||
</legend>
|
|
||||||
<table class="items centered">
|
|
||||||
<tbody id="readouts">
|
|
||||||
<tr id="new_readouts_actions">
|
|
||||||
<td colspan="4"><div class="actions centered"><%= form.button %></div></td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</fieldset>
|
|
||||||
<% end %>
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
<%= turbo_stream.update(:new_readouts_form_legend) { @common_ancestor&.pathname } %>
|
|
||||||
|
|
||||||
<% @prev_quantities.each do |pq| %>
|
|
||||||
<%= turbo_stream.update dom_id(pq, nil, :pathname) do %>
|
|
||||||
<%= pq.relative_pathname(@common_ancestor) %>
|
|
||||||
<% end %>
|
|
||||||
<% end %>
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
<%= turbo_stream.remove dom_id(@quantity, :new, :readout) %>
|
|
||||||
<%= render partial: 'form_repath' %>
|
|
||||||
@@ -1,20 +1,14 @@
|
|||||||
<div class="topside vflex">
|
<%# TODO: show hint when no quantities/units defined %>
|
||||||
|
<div class="rightside buttongrid">
|
||||||
<% if current_user.at_least(:active) %>
|
<% if current_user.at_least(:active) %>
|
||||||
<%# TODO: show hint when no quantities/units defined %>
|
<%= image_link_to t('.new_measurement'), 'plus-outline', new_measurement_path,
|
||||||
<%= tabular_form_with url: new_measurement_path,
|
id: :new_measurement_link, onclick: 'this.blur();',
|
||||||
html: {id: :new_readouts_form} do |f| %>
|
data: {turbo_stream: true} %>
|
||||||
<% end %>
|
|
||||||
<div class="hflex">
|
|
||||||
<%= select_tag :id, options_from_collection_for_select(
|
|
||||||
@quantities, :id, ->(q){ sanitize(' ' * q.depth + q.name) }
|
|
||||||
), form: :new_readouts_form %>
|
|
||||||
<% common_options = {form: :new_readouts_form, formmethod: :get,
|
|
||||||
formnovalidate: true, data: {turbo_stream: true}} %>
|
|
||||||
<%= image_button_tag t('.new_quantity'), 'plus-outline', **common_options -%>
|
|
||||||
<%= image_button_tag t('.new_children'), 'plus-multiple-outline',
|
|
||||||
formaction: new_measurement_path(:children), **common_options -%>
|
|
||||||
<%= image_button_tag t('.new_subtree'), 'plus-multiple-outline',
|
|
||||||
formaction: new_measurement_path(:subtree), **common_options -%>
|
|
||||||
</div>
|
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<table class="main">
|
||||||
|
<tbody id="measurements">
|
||||||
|
<%= render(@measurements) || render_no_items %>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|||||||
@@ -1,9 +1,5 @@
|
|||||||
<%= turbo_stream.update :new_readouts_form do %>
|
<%= turbo_stream.disable :new_measurement_link -%>
|
||||||
<%= render partial: 'form_frame' %>
|
<%= turbo_stream.hide :no_items -%>
|
||||||
<% end if @prev_quantities.empty? %>
|
<%= turbo_stream.append_all 'body' do %>
|
||||||
|
|
||||||
<%= render partial: 'form_repath' %>
|
|
||||||
|
|
||||||
<%= turbo_stream.before :new_readouts_actions do %>
|
|
||||||
<%= render partial: 'form' %>
|
<%= render partial: 'form' %>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<%= tabular_fields_for @quantity, form: form_tag do |form| %>
|
<%= tabular_fields_for @quantity, form: form_tag do |form| %>
|
||||||
<%- tag.tr id: row, class: "form", onkeydown: "processKey(event)",
|
<%- tag.tr id: row, class: "form", onkeydown: "formProcessKey(event)",
|
||||||
data: {link: link, form: form_tag, hidden_row: hidden_row} do %>
|
data: {link: link, form: form_tag, hidden_row: hidden_row} do %>
|
||||||
|
|
||||||
<td style="--depth:<%= @quantity.depth %>">
|
<td style="--depth:<%= @quantity.depth %>">
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
<div class="rightside buttongrid">
|
<div class="rightside buttongrid">
|
||||||
<% if current_user.at_least(:active) %>
|
<% if current_user.at_least(:active) %>
|
||||||
<%= image_link_to t('.new_quantity'), 'plus-outline', new_quantity_path,
|
<%= image_link_to t('.new_quantity'), 'plus-outline', new_quantity_path,
|
||||||
id: dom_id(Quantity, :new, :link), onclick: 'this.blur();', data: {turbo_stream: true} %>
|
id: dom_id(Quantity, :new, :link), onclick: 'this.blur();',
|
||||||
|
data: {turbo_stream: true} %>
|
||||||
<% end %>
|
<% end %>
|
||||||
<%#= image_link_to t('.import_quantities'), 'download-outline', default_quantities_path,
|
<%#= image_link_to t('.import_quantities'), 'download-outline', default_quantities_path,
|
||||||
class: 'tools' %>
|
class: 'tools' %>
|
||||||
|
|||||||
25
app/views/readouts/_form.html.erb
Normal file
25
app/views/readouts/_form.html.erb
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
<%# TODO: add readout reordering by dragging %>
|
||||||
|
<%= tabular_fields_for 'readouts[]', readout do |form| %>
|
||||||
|
<%- tag.tr id: dom_id(readout.quantity, :new, :readout) do %>
|
||||||
|
<td class="actions">
|
||||||
|
<%# TODO: change to _link_ after giving up displaying relative paths %>
|
||||||
|
<%= image_button_tag '', 'delete-outline', class: 'dangerous', name: nil,
|
||||||
|
formaction: discard_readouts_path(readout.quantity),
|
||||||
|
formmethod: :get, formnovalidate: true, data: {turbo_stream: true} %>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<%= readout.quantity.relative_pathname(@superquantity) %>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<%= form.number_field :value, required: true,
|
||||||
|
size: readout.type_for_attribute(:value).precision / 2,
|
||||||
|
autofocus: readout_counter == 0 %>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<%= form.hidden_field :quantity_id %>
|
||||||
|
<%= form.collection_select :unit_id, @user_units, :id,
|
||||||
|
->(u){ sanitize(' ' * (u.base_id ? 1 : 0) + u.symbol) },
|
||||||
|
{prompt: t('.select_unit'), disabled: '', selected: ''}, required: true %>
|
||||||
|
</td>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
7
app/views/readouts/_form_repath.html.erb
Normal file
7
app/views/readouts/_form_repath.html.erb
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<%= turbo_stream.update(:measurement_form_legend) { @superquantity&.pathname } %>
|
||||||
|
|
||||||
|
<% @prev_quantities.each do |pq| %>
|
||||||
|
<%= turbo_stream.update dom_id(pq, nil, :pathname) do %>
|
||||||
|
<%= pq.relative_pathname(@superquantity) %>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
4
app/views/readouts/discard.turbo_stream.erb
Normal file
4
app/views/readouts/discard.turbo_stream.erb
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<%= turbo_stream.disable :create_measurement_button if @prev_quantities.one? %>
|
||||||
|
<%= turbo_stream.remove dom_id(@quantity, :new, :readout) %>
|
||||||
|
<%= render partial: 'form_repath' %>
|
||||||
|
<%= turbo_stream.unselect dom_id(@quantity) %>
|
||||||
8
app/views/readouts/new.turbo_stream.erb
Normal file
8
app/views/readouts/new.turbo_stream.erb
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<% @readouts.each do |r| %>
|
||||||
|
<%= turbo_stream.disable dom_id(r.quantity) %>
|
||||||
|
<% end %>
|
||||||
|
<%= render partial: 'form_repath' %>
|
||||||
|
<%= turbo_stream.append :readouts do %>
|
||||||
|
<%= render partial: 'form', collection: @readouts, as: :readout %>
|
||||||
|
<% end %>
|
||||||
|
<%= turbo_stream.enable :create_measurement_button if @prev_quantities.empty? %>
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<%= tabular_fields_for @unit, form: form_tag do |form| %>
|
<%= tabular_fields_for @unit, form: form_tag do |form| %>
|
||||||
<%- tag.tr id: row, class: "form", onkeydown: "processKey(event)",
|
<%- tag.tr id: row, class: "form", onkeydown: "formProcessKey(event)",
|
||||||
data: {link: link, form: form_tag, hidden_row: hidden_row} do %>
|
data: {link: link, form: form_tag, hidden_row: hidden_row} do %>
|
||||||
|
|
||||||
<td style="--depth:<%= @unit.base_id? ? 1 : 0 %>">
|
<td style="--depth:<%= @unit.base_id? ? 1 : 0 %>">
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
require 'core_ext/array_delete_bang'
|
||||||
require 'core_ext/big_decimal_scientific_notation'
|
require 'core_ext/big_decimal_scientific_notation'
|
||||||
|
|
||||||
ActiveSupport.on_load :action_dispatch_system_test_case do
|
ActiveSupport.on_load :action_dispatch_system_test_case do
|
||||||
|
|||||||
@@ -19,7 +19,19 @@ ActiveSupport.on_load :turbo_streams_tag_builder do
|
|||||||
action :hide, target, allow_inferred_rendering: false
|
action :hide, target, allow_inferred_rendering: false
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def show(target)
|
||||||
|
action :show, target, allow_inferred_rendering: false
|
||||||
|
end
|
||||||
|
|
||||||
|
#def collapse(target)
|
||||||
|
# action :collapse, target, allow_inferred_rendering: false
|
||||||
|
#end
|
||||||
|
|
||||||
def close_form(target)
|
def close_form(target)
|
||||||
action :close_form, target, allow_inferred_rendering: false
|
action :close_form, target, allow_inferred_rendering: false
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def unselect(target)
|
||||||
|
action :unselect, target, allow_inferred_rendering: false
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -64,10 +64,14 @@ en:
|
|||||||
source_code: Get code
|
source_code: Get code
|
||||||
measurements:
|
measurements:
|
||||||
navigation: Measurements
|
navigation: Measurements
|
||||||
|
no_items: There are no measurements taken. You can Add some now.
|
||||||
|
form:
|
||||||
|
select_quantity: select the measured quantities...
|
||||||
index:
|
index:
|
||||||
new_quantity: Selected
|
new_measurement: Add measurement
|
||||||
new_children: Children
|
readouts:
|
||||||
new_subtree: Subtree
|
form:
|
||||||
|
select_unit: ...
|
||||||
quantities:
|
quantities:
|
||||||
navigation: Quantities
|
navigation: Quantities
|
||||||
no_items: There are no configured quantities. You can Add some or Import from defaults.
|
no_items: There are no configured quantities. You can Add some or Import from defaults.
|
||||||
@@ -149,6 +153,7 @@ en:
|
|||||||
other: (%{count} characters minimum)
|
other: (%{count} characters minimum)
|
||||||
actions: Actions
|
actions: Actions
|
||||||
add: Add
|
add: Add
|
||||||
|
apply: Apply
|
||||||
back: Back
|
back: Back
|
||||||
cancel: Cancel
|
cancel: Cancel
|
||||||
delete: Delete
|
delete: Delete
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
Rails.application.routes.draw do
|
Rails.application.routes.draw do
|
||||||
resources :measurements, path_names: {new: '/new(/:scope)'},
|
resources :measurements
|
||||||
constraints: {scope: /children|subtree/}, defaults: {scope: nil} do
|
|
||||||
|
|
||||||
get 'discard/:id', on: :new, action: :discard, as: :discard
|
resources :readouts, only: [:new] do
|
||||||
|
collection {get 'new/:id/discard', action: :discard, as: :discard}
|
||||||
end
|
end
|
||||||
|
|
||||||
resources :quantities, except: [:show], path_names: {new: '(/:id)/new'} do
|
resources :quantities, except: [:show], path_names: {new: '(/:id)/new'} do
|
||||||
|
|||||||
10
lib/core_ext/array_delete_bang.rb
Normal file
10
lib/core_ext/array_delete_bang.rb
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
module CoreExt
|
||||||
|
module ArrayDeleteBang
|
||||||
|
def delete!(obj)
|
||||||
|
self.delete(obj)
|
||||||
|
self
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
Array.prepend CoreExt::ArrayDeleteBang
|
||||||
Reference in New Issue
Block a user