CRUD

This section illustrates how to build a full CRUD interface using Lato features.

Model

class Product < ApplicationRecord
  enum :status, {
    created: 0,
    in_progress: 1,
    completed: 2,
    cancelled: 3
  }, suffix: true

  attr_reader :actions

  # Relations
  ##

  belongs_to :lato_user, class_name: 'Lato::User'
  belongs_to :product_parent, class_name: 'Product', optional: true
  has_many :product_children, class_name: 'Product', foreign_key: :product_parent_id

  # Scopes
  ##

  scope :lato_index_order, ->(column, order) do
    return joins(:lato_user).order("lato_users.last_name #{order}, lato_users.first_name #{order}") if column == :lato_user_id

    order("#{column} #{order}")
  end

  scope :lato_index_search, ->(search) do
    joins(:lato_user).where("lower(code) LIKE :search OR lower(lato_users.first_name) LIKE :search OR lower(lato_users.last_name) LIKE :search", search: "%#{search.downcase.strip}%")
  end

  # Hooks
  ##

  before_create do
    self.status ||= :created
  end

  # Helpers
  ##

  def lifetime
    Time.now - created_at
  end

  def status_color
    return 'warning' if created_status?
    return 'primary' if in_progress_status?
    return 'success' if completed_status?
    return 'danger' if cancelled_status?

    'secondary'
  end
end

Controller

class ProductsController < ApplicationController
  before_action :authenticate_session
  before_action { active_sidebar(:products) }

  def index
    columns = %i[id code status product_parent_id lato_user_id custom_dynamic_column created_at actions]
    sortable_columns = %i[id code status lato_user_id]
    searchable_columns = %i[id code lato_user_id]

    @products = lato_index_collection(
      Product.all.includes(:lato_user),
      columns: columns,
      sortable_columns: sortable_columns,
      searchable_columns: searchable_columns,
      default_sort_by: 'code|ASC',
      pagination: 10,
    )
  end

  # NOTE: This endpoint is used for inputs with autocomplete feature to select a product using search.
  def index_autocomplete
    unless params[:value].blank?
      @product = Product.find_by(id: params[:value])
      return render json: @product ? { label: @product.code, value: @product.id } : nil
    end

    @products = Product.where('code LIKE ?', "#{params[:q]}%").limit(10)
    render json: @products.map { |product| { label: product.code, value: product.id } }
  end

  def create
    @product = Product.new
  end

  def create_action
    @product = Product.new(product_params.merge(lato_user_id: @session.user_id))

    respond_to do |format|
      if @product.save
        format.html { redirect_to main_app.products_path, notice: 'Product created successfully' }
        format.json { render json: @product }
      else
        format.html { render :create, status: :unprocessable_entity }
        format.json { render json: @product.errors, status: :unprocessable_entity }
      end
    end
  end

  def update
    @product = Product.find(params[:id])
  end

  def update_action
    @product = Product.find(params[:id])

    respond_to do |format|
      if @product.update(product_params)
        format.html { redirect_to main_app.products_path, notice: 'Product updated successfully' }
        format.json { render json: @product }
      else
        format.html { render :update, status: :unprocessable_entity }
        format.json { render json: @product.errors, status: :unprocessable_entity }
      end
    end
  end

  def export_action
    @operation = Lato::Operation.generate('ExportProductsJob', {}, @session.user_id)

    respond_to do |format|
      if @operation.start
        format.html { redirect_to lato.operation_path(@operation) }
        format.json { render json: @operation }
      else
        format.html { render :index, status: :unprocessable_entity }
        format.json { render json: @operation.errors, status: :unprocessable_entity }
      end
    end
  end

  private

  def product_params
    params.require(:product).permit(:code, :product_parent_id)
  end
end

Views

Index

<%= lato_page_head 'Products', [
  { label: 'Products' }
] %>

<%= lato_index @products,
  custom_actions: {
    create: {
      path: products_create_path,
      icon: 'bi bi-plus',
      title: 'Create product',
      data: {
        controller: 'lato-tooltip',
        lato_action_target: 'trigger',
        turbo_frame: dom_id(Product.new, 'form'),
        action_title: 'Create product'
      }
    },
    export: {
      path: products_export_action_path,
      icon: 'bi bi-download',
      title: 'Export products',
      data: {
        controller: 'lato-tooltip',
        lato_action_target: 'trigger',
        turbo_method: :post,
        turbo_frame: 'lato_operation'
      }
    },
  },
  dropdown_actions: {
    icon: 'bi bi-three-dots',
    title: 'More actions',
    data: {
      controller: 'lato-tooltip',
    },
    actions: [
      {
        path: '#',
        icon: 'bi bi-trash',
        title: 'Example 1',
        data: {
          custom: 'value',
        }
      },
      {
        path: '#',
        icon: 'bi bi-download',
        title: 'Example 2',
        data: {
          custom: 'value',
        }
      }
    ]
  },
  pagination_options: [10,20,50,100]
%>

Create

<%= lato_page_head 'New product', [
  { label: 'Products', path: products_path },
  { label: 'New product' }
] %>

<div class="card mb-4">
  <div class="card-header">
    <h2 class="fs-4 mb-0">New product registration</h2>
  </div>
  <div class="card-body">
    <%= render 'products/form', product: @product %>
  </div>
</div>

Update

<%= lato_page_head 'Edit product', [
  { label: 'Products', path: products_path },
  { label: 'Edit product' }
] %>

<div class="card mb-4">
  <div class="card-header">
    <h2 class="fs-4 mb-0">Edit product</h2>
  </div>
  <div class="card-body">
    <%= render 'products/form', product: @product %>
  </div>
</div>

Partial Form

<%

product ||= Product.new

%>

<%= turbo_frame_tag dom_id(product, 'form') do %>
  <%= form_with model: product, url: product.persisted? ? products_update_action_path(product) : products_create_action_path, data: { turbo_frame: '_self', controller: 'lato-form' } do |form| %>
    <%= lato_form_notices class: %w[mb-3] %>
    <%= lato_form_errors product, class: %w[mb-3] %>

    <div class="mb-3">
      <%= lato_form_item_label form, :code %>
      <%= lato_form_item_input_text form, :code, required: true, data: { controller: 'lato-input-autocomplete', lato_input_autocomplete_path_value: products_autocomplete_path } %>
    </div>

    <div class="mb-3">
      <%= lato_form_item_label form, :product_parent_id %>
      <%= lato_form_item_input_text form, :product_parent_id, required: true, data: { controller: 'lato-input-autocomplete2', lato_input_autocomplete2_path_value: products_autocomplete_path } %>
    </div>

    <div class="d-flex justify-content-end">
      <%= lato_form_submit form, product.persisted? ? 'Update' : 'Confirm', class: %w[btn-success] %>
    </div>
  <% end %>
<% end %>

Helpers

module ProductsHelper
  def product_status(product)
    lato_data_badge(Product.human_attribute_name("status.#{product.status}"), product.status_color)
  end

  def product_created_at(product)
    product.created_at.strftime('%d/%m/%Y')
  end

  def product_lato_user_id(product)
    lato_data_user(product.lato_user.full_name, product.lato_user.gravatar_image_url(50))
  end

  def product_product_parent_id(product)
    return ' - ' if product.product_parent.nil?

    product.product_parent.code
  end

  def product_lifetime(product)
    Time.at(product.lifetime).utc.strftime('%H h %M m')
  end

  def product_actions(product)
    content_tag(:div, class: 'btn-group btn-group-sm') do
      concat link_to('Edit', products_update_path(product), class: 'btn btn-primary', data: { lato_action_target: 'trigger', turbo_frame: dom_id(product, 'form'), action_title: 'Edit product' })
      concat link_to('Delete', '#', class: 'btn btn-danger', data: { turbo_method: 'POST', turbo_confirm: 'Are you sure to continue?' })
    end
  end
end

You are offline You are online