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
has_many :product_items
accepts_nested_attributes_for :product_items, allow_destroy: true, reject_if: proc { |attributes| attributes["name"].blank? }
# 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 destroy_action
@product = Product.find(params[:id])
respond_to do |format|
if @product.destroy
format.html { redirect_to main_app.products_path, notice: 'Product deleted successfully' }
format.json { render json: { message: 'Product deleted successfully' } }
else
format.html { redirect_to main_app.products_path, alert: 'Failed to delete product' }
format.json { render json: { error: 'Failed to delete product' }, 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, product_items_attributes: [
:id, :name, :quantity, :_destroy
])
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="mb-3">
<%= lato_form_item_label form, :product_items %>
<%= lato_form_item_input_list form, :product_items, 'products/input_list_product_item' %>
</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', products_destroy_action_path(product), class: 'btn btn-danger', data: { turbo_method: 'DELETE', turbo_confirm: 'Are you sure to continue?' })
end
end
end