From 57f10c94a451aa820ac5e672e1954681c955828d Mon Sep 17 00:00:00 2001 From: cryptogopher Date: Sat, 11 Jan 2025 17:01:34 +0100 Subject: [PATCH] Add Quantity #new, #create, #destroy actions --- app/controllers/quantities_controller.rb | 35 +++++++++++++ app/models/quantity.rb | 52 ++++++++++++++----- app/views/quantities/_form.html.erb | 19 +++++++ app/views/quantities/_form_close.html.erb | 2 + app/views/quantities/_quantity.html.erb | 28 ++++++++++ app/views/quantities/create.turbo_stream.erb | 7 +++ app/views/quantities/destroy.turbo_stream.erb | 5 ++ app/views/quantities/new.turbo_stream.erb | 20 +++++++ config/locales/en.yml | 11 ++-- config/routes.rb | 4 +- 10 files changed, 166 insertions(+), 17 deletions(-) create mode 100644 app/views/quantities/_form.html.erb create mode 100644 app/views/quantities/_form_close.html.erb create mode 100644 app/views/quantities/_quantity.html.erb create mode 100644 app/views/quantities/create.turbo_stream.erb create mode 100644 app/views/quantities/destroy.turbo_stream.erb create mode 100644 app/views/quantities/new.turbo_stream.erb diff --git a/app/controllers/quantities_controller.rb b/app/controllers/quantities_controller.rb index 50c573c..5eda6c2 100644 --- a/app/controllers/quantities_controller.rb +++ b/app/controllers/quantities_controller.rb @@ -1,4 +1,9 @@ class QuantitiesController < ApplicationController + before_action only: :new do + find_quantity if params[:id].present? + end + before_action :find_quantity, only: [:edit, :update, :rebase, :destroy] + before_action except: :index do raise AccessForbidden unless current_user.at_least(:active) end @@ -6,4 +11,34 @@ class QuantitiesController < ApplicationController def index @quantities = current_user.quantities.includes(:parent).includes(:subquantities).ordered end + + def new + @quantity = current_user.quantities.new(parent: @quantity) + end + + def create + @quantity = current_user.quantities.new(quantity_params) + if @quantity.save + @before, @quantity, @ancestors = @quantity.succ_and_ancestors + flash.now[:notice] = t('.success', quantity: @quantity) + else + render :new + end + end + + def destroy + @quantity.destroy! + @ancestors = @quantity.ancestors + flash.now[:notice] = t('.success', quantity: @quantity) + end + + private + + def quantity_params + params.require(:quantity).permit(Quantity::ATTRIBUTES) + end + + def find_quantity + @quantity = Quantity.find_by!(id: params[:id], user: current_user) + end end diff --git a/app/models/quantity.rb b/app/models/quantity.rb index d62b815..89c47b5 100644 --- a/app/models/quantity.rb +++ b/app/models/quantity.rb @@ -1,4 +1,6 @@ class Quantity < ApplicationRecord + ATTRIBUTES = [:name, :description, :parent_id] + belongs_to :user, optional: true belongs_to :parent, optional: true, class_name: "Quantity" has_many :subquantities, class_name: "Quantity", inverse_of: :parent, @@ -38,7 +40,7 @@ class Quantity < ApplicationRecord Arel::Nodes::NamedFunction.new('ROW_NUMBER', []) .over(Arel::Nodes::Window.new.partition(parent_column).order(order_column)), Arel::SelectManager.new.project( - Arel::Nodes::NamedFunction.new('LENGTH', [Arel.star.count])]) + Arel::Nodes::NamedFunction.new('LENGTH', [Arel.star.count]) ), Arel::Nodes.build_quoted('0') ], @@ -51,30 +53,54 @@ class Quantity < ApplicationRecord end def movable? - subunits.empty? + subquantities.empty? end def default? parent_id.nil? end - # Return: record, its ancestors and succesive record in order of appearance, - # including :depth attribute. Used for table view reload. - def successive + # Return: self, ancestors and successive record in order of appearance, + # including :depth attribute. Used for partial view reload. + def succ_and_ancestors quantities = Quantity.arel_table ancestors = Arel::Table.new('ancestors') + Quantity.with( ancestors: user.quantities.ordered.select( Arel::Nodes::NamedFunction.new('LAG', [quantities[:id]]).over.as('lag_id') ) ) - .with_recursive(quantities: [ - Arel::SelectManager.new.project(ancestors[Arel.star]).from(ancestors) - .where(ancestors[:id].eq(id).or(ancestors[:lag_id].eq(id))), - Arel::SelectManager.new.project(ancestors[Arel.star]).from(ancestors) - .join(quantities).on(quantities[:parent_id].eq(ancestors[:id])) - .where(quantities[:lag_id].not_eq(id)) - ]).order(quantities[:path]) - # return: .first == self ? nul, ancestors : ancestors.pop, ancestors + .with_recursive(quantities: [ + Arel::SelectManager.new.project(ancestors[Arel.star]).from(ancestors) + .where(ancestors[:id].eq(id).or(ancestors[:lag_id].eq(id))), + Arel::SelectManager.new.project(ancestors[Arel.star]).from(ancestors) + .join(quantities).on(quantities[:parent_id].eq(ancestors[:id])) + .where(quantities[:lag_id].not_eq(id))]) + .order(quantities[:path]).to_a + .then do |records| + [records.last == self ? nil : records.pop, records.pop, records] + end + end + + def ancestors + quantities = Quantity.arel_table + ancestors = Arel::Table.new('ancestors') + + # Ancestors are listed bottom up, so it's impossible to know depth at the + # start. Start with depth = 0 and count downwards, then adjust by the + # amount needed to set biggest negative depth to 0. + Quantity.with_recursive(ancestors: [ + user.quantities.select(quantities[Arel.star], Arel::Nodes.build_quoted(0).as('depth')) + .where(id: parent_id), + user.quantities.select(quantities[Arel.star], ancestors[:depth] - 1) + .joins(quantities.create_join( + ancestors, quantities.create_on(quantities[:id].eq(ancestors[:parent_id])) + )) + ]).select(ancestors[Arel.star]).from(ancestors).to_a.then do |records| + records.map(&:depth).min&.abs.then do |maxdepth| + records.each { |r| r.depth += maxdepth } + end + end end end diff --git a/app/views/quantities/_form.html.erb b/app/views/quantities/_form.html.erb new file mode 100644 index 0000000..a08128a --- /dev/null +++ b/app/views/quantities/_form.html.erb @@ -0,0 +1,19 @@ +<%= tabular_fields_for @quantity, form: form_tag do |form| %> + <%- tag.tr id: row, class: "form", onkeydown: "processKey(event)", + data: {link: link, form: form_tag, hidden_row: hidden_row} do %> + + + <%= form.text_field :name, required: true, autofocus: true, size: 20 %> + + + <%= form.text_area :description, cols: 30, rows: 1, escape: false %> + + + + <%= form.button %> + <%= image_link_to t(:cancel), "close-outline", quantities_path, class: 'dangerous', + name: :cancel, onclick: render_turbo_stream('form_close', {row: row}) %> + + + <% end %> +<% end %> diff --git a/app/views/quantities/_form_close.html.erb b/app/views/quantities/_form_close.html.erb new file mode 100644 index 0000000..e79ea3c --- /dev/null +++ b/app/views/quantities/_form_close.html.erb @@ -0,0 +1,2 @@ +<%= turbo_stream.close_form row %> +<%= turbo_stream.update :flashes %> diff --git a/app/views/quantities/_quantity.html.erb b/app/views/quantities/_quantity.html.erb new file mode 100644 index 0000000..bd6a394 --- /dev/null +++ b/app/views/quantities/_quantity.html.erb @@ -0,0 +1,28 @@ +<%= tag.tr id: dom_id(quantity), + ondragstart: 'dragStart(event)', ondragend: 'dragEnd(event)', + ondragover: 'dragOver(event)', ondrop: 'drop(event)', + ondragenter: 'dragEnter(event)', ondragleave: 'dragLeave(event)', + data: {drag_path: rebase_quantity_path(quantity), + drop_id: dom_id(quantity.parent || quantity)} do %> + + + <%= link_to quantity, edit_quantity_path(quantity), onclick: 'this.blur();', + data: {turbo_stream: true} %> + + <%= quantity.description %> + + <% if current_user.at_least(:active) %> + + <%= image_link_to t('.new_subquantity'), 'plus-outline', new_quantity_path(quantity), + id: dom_id(quantity, :new, :link), onclick: 'this.blur();', data: {turbo_stream: true} %> + + <%= image_button_to_if quantity.movable?, t('.destroy'), 'delete-outline', + quantity_path(quantity), method: :delete %> + + <% if quantity.movable? %> + ⠿ + <% else %> + + <% end %> + <% end %> +<% end %> diff --git a/app/views/quantities/create.turbo_stream.erb b/app/views/quantities/create.turbo_stream.erb new file mode 100644 index 0000000..b73eba5 --- /dev/null +++ b/app/views/quantities/create.turbo_stream.erb @@ -0,0 +1,7 @@ +<%= turbo_stream.close_form dom_id(@quantity.parent || Quantity, :new) %> +<%= turbo_stream.remove :no_items %> +<% @ancestors.map do |ancestor| %> + <%= turbo_stream.replace ancestor %> +<% end %> +<%= @before.nil? ? turbo_stream.append(:quantities, @quantity) : + turbo_stream.before(@before, @quantity) %> diff --git a/app/views/quantities/destroy.turbo_stream.erb b/app/views/quantities/destroy.turbo_stream.erb new file mode 100644 index 0000000..5330055 --- /dev/null +++ b/app/views/quantities/destroy.turbo_stream.erb @@ -0,0 +1,5 @@ +<% @ancestors.map do |ancestor| %> + <%= turbo_stream.replace ancestor %> +<% end %> +<%= turbo_stream.remove @quantity %> +<%= turbo_stream.append(:quantities, render_no_items) if current_user.quantities.empty? %> diff --git a/app/views/quantities/new.turbo_stream.erb b/app/views/quantities/new.turbo_stream.erb new file mode 100644 index 0000000..9ebb8e2 --- /dev/null +++ b/app/views/quantities/new.turbo_stream.erb @@ -0,0 +1,20 @@ +<% dom_obj = @quantity.parent || @quantity %> +<% ids = {row: dom_id(dom_obj, :new), + hidden_row: nil, + link: dom_id(dom_obj, :new, :link), + form_tag: dom_id(dom_obj, :new, :form)} %> + +<%= turbo_stream.disable ids[:link] -%> + +<%= turbo_stream.append :quantity_form do %> + <%- tabular_form_with model: @quantity, html: {id: ids[:form_tag]} do |form| %> + <%= form.hidden_field :parent_id unless @quantity.parent.nil? %> + <% end %> +<% end %> + +<% if @quantity.parent %> + <%= turbo_stream.remove ids[:row] %> + <%= turbo_stream.after @quantity.parent, partial: 'form', locals: ids %> +<% else %> + <%= turbo_stream.prepend :quantities, partial: 'form', locals: ids %> +<% end %> diff --git a/config/locales/en.yml b/config/locales/en.yml index 171c407..7527933 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -56,8 +56,15 @@ en: quantities: navigation: Quantities no_items: There are no configured quantities. You can Add some or Import from defaults. + quantity: + new_subquantity: Child + destroy: Delete index: - new_quantity: New quantity + new_quantity: Add quantity + create: + success: Created new quantity "%{quantity}" + destroy: + success: Deleted quantity "%{quantity}" units: navigation: Units no_items: There are no configured units. You can Add some or Import from defaults. @@ -68,8 +75,6 @@ en: new_unit: Add unit import_units: Import top_level_drop: Drop here to reposition into top-level unit - new: - none: none create: success: Created new unit "%{unit}" update: diff --git a/config/routes.rb b/config/routes.rb index 1d13054..5bab666 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,5 +1,7 @@ Rails.application.routes.draw do - resources :quantities + resources :quantities, except: [:show], path_names: {new: '(/:id)/new'} do + member { post :rebase } + end resources :units, except: [:show], path_names: {new: '(/:id)/new'} do member { post :rebase }