module CmAdmin
module Models
module DslMethod
extend ActiveSupport::Concern
# Create a table for index page with pagination.
#
# @param page_title [String] the title of page
# @param page_description [String] the description of page
# @param partial [String] the partial path of page
# @param view_type [Symbol] view type of page +:table+, +:card+ or +:kanban+
# @example Index page
# cm_index do
# page_title 'Post'
# column :title
# column :created_at, field_type: :date, format: '%d %b, %Y'
# column :updated_at, field_type: :date, format: '%d %b, %Y', header: 'Last Updated At'
# end
# rdoc-image:/public/examples/cm_index.png
def cm_index(page_title: nil, page_description: nil, partial: nil, view_type: :table)
@current_action = CmAdmin::Models::Action.find_by(self, name: 'index')
@current_action.set_values(page_title, page_description, partial, view_type)
yield
end
# Create a view for show page
#
# @param page_title [String | Symbol] the title of page, if symbol passed, it will be a method name on model
# @param page_description [String] the description of page
# @param partial [String] the partial path of page
#
# @example Showing page
# cm_show page_title: :title do
# tab :profile, '' do
# cm_section 'Post Details' do
# field :title
# field :body, field_type: :rich_text
# field :is_featured
# field :status, field_type: :tag, tag_class: STATUS_TAG_COLOR
# end
# end
# end
def cm_show(page_title: nil, page_description: nil, partial: nil)
@current_action = CmAdmin::Models::Action.find_by(self, name: 'show')
@current_action.set_values(page_title, page_description, partial)
yield
end
# Create a form for edit action
#
# @param page_title [String] or [Symbol] the title of page, if symbol passed, it will be a method name on model
# @param page_description [String] the description of page
# @param partial [String] the partial path of page
# @param redirect_to [Proc, nil] A lambda that takes the current object and redirect to path after update
#
# @example Editing page with a redirect
# cm_edit(page_title: "Edit Post", page_description: 'Enter all details to edit Post', redirect_to: ->(current_object) { "/pages/#{current_object.id}" }) do
# cm_section 'Details' do
# form_field :title, input_type: :string
# form_field :body, input_type: :rich_text
# end
# end
def cm_edit(page_title: nil, page_description: nil, partial: nil, redirect_to: nil)
@current_action = CmAdmin::Models::Action.find_by(self, name: 'edit')
@current_action.set_values(page_title, page_description, partial, redirect_to)
yield
end
# Create a form for new action
#
# @param page_title [String] the title of page
# @param page_description [String] the description of page
# @param partial [String] the partial path of page
# @param redirect_to [Proc, nil] A lambda that takes the current object and redirect to path after create
#
# @example Creating a new page with a redirect
# cm_new(page_title: "Add Post", page_description: 'Enter all details to add Post', redirect_to: ->(current_object) { "/pages/#{current_object.id}" }) do
# cm_section 'Details' do
# form_field :title, input_type: :string
# form_field :body, input_type: :rich_text
# end
# end
def cm_new(page_title: nil, page_description: nil, partial: nil, redirect_to: nil)
@current_action = CmAdmin::Models::Action.find_by(self, name: 'new')
@current_action.set_values(page_title, page_description, partial, redirect_to)
yield
end
# Set page title for current action
# @param title [String] the title of page
def page_title(title)
return unless @current_action
@current_action.page_title = title
end
# Set page description for current action
# @param description [String] the description of page
def page_description(description)
return unless @current_action
@current_action.page_description = description
end
# Set kanban view for current action
# @param column_name [String] the name of column
# @param exclude [Array] the array of fields to exclude
# @param only [Array] the array of fields to include
def kanban_view(column_name, exclude: [], only: [])
return unless @current_action
@current_action.kanban_attr[:column_name] = column_name
@current_action.kanban_attr[:exclude] = exclude
@current_action.kanban_attr[:only] = only
end
# Set scopes for current action
# @param scopes [Array] the array of scopes
def scope_list(scopes = [])
return unless @current_action
@current_action.scopes = scopes
end
# Create a new tab on show page.
#
# @param tab_name [String] or [Symbol] the name of tab
# @param custom_action [String] the name of custom action
# @param associated_model [String] the name of associated model
# @param layout_type [String] the layout type of tab, +cm_association_index+, +cm_association_show+
# @param layout [String] the layout of tab
# @param partial [String] the partial path of tab
# @param display_if [Proc] A lambda that takes the current object and return true or false
#
# @example Creating a tab
# tab :comments, 'comment', associated_model: 'comments', layout_type: 'cm_association_index' do
# column :message
# end
def tab(tab_name, custom_action, associated_model: nil, layout_type: nil, layout: nil, partial: nil, display_if: nil, &block)
if custom_action.to_s == ''
@current_action = CmAdmin::Models::Action.find_by(self, name: 'show')
@available_tabs << CmAdmin::Models::Tab.new(tab_name, '', display_if, &block)
else
action = CmAdmin::Models::Action.new(name: custom_action.to_s, verb: :get, path: ':id/' + custom_action,
layout_type:, layout:, partial:, child_records: associated_model,
action_type: :custom, display_type: :page, model_name: name)
@available_actions << action
@current_action = action
@available_tabs << CmAdmin::Models::Tab.new(tab_name, custom_action, display_if, &block)
end
yield if block
end
# Create a new row on page or form.
# @param display_if [Proc] A lambda that takes the current object and return true or false
# @param html_attrs [Hash] A hash that contains html attributes
# @example Creating a row
# row(display_if: ->(current_object) { current_object.name == 'John' }, html_attrs: { class: 'row-class' }) do
# cm_section 'Details' do
# form_field :title, input_type: :string
# end
# end
def row(display_if: nil, html_attrs: nil, &block)
@available_fields[@current_action.name.to_sym] ||= []
@available_fields[@current_action.name.to_sym] << CmAdmin::Models::Row.new(@current_action, @model, display_if, html_attrs, &block)
end
# Create a new section on page or form.
# @param section_name [String] the name of section
# @param display_if [Proc] A lambda that takes the current object and return true or false
# @param col_size [Integer] the size of column according to grid view
# @param html_attrs [Hash] A hash that contains html attributes
# @example Creating a section
# cm_section('Basic Information', display_if: ->(current_object) { current_object.name == 'John' }, col_size: 6, html_attrs: { class: 'section-class' }) do
# field :title, input_type: :string
# end
def cm_section(section_name, display_if: nil, col_size: nil, html_attrs: nil, partial: nil, &block)
@available_fields[@current_action.name.to_sym] ||= []
@available_fields[@current_action.name.to_sym] << CmAdmin::Models::Section.new(section_name, @current_action, @model, display_if, html_attrs, col_size, partial, &block)
end
# @deprecated Use {#cm_section} instead of this method
def cm_show_section(section_name, display_if: nil, html_attrs: nil, partial: nil, &block)
cm_section(section_name, display_if:, html_attrs:, partial:, &block)
end
# Create a new column on index layout.
# @param field_name [String] the name of field
# @param field_type [Symbol] the type of field, +:string+, +:text+, +:image+, +:date+, +:rich_text+, +:time+, +:integer+, +:decimal+, +:custom+, +:datetime+, +:money+, +:money_with_symbol+, +:link+, +:association+, +:enum+, +:tag+, +:attachment+, +:drawer+
# @param header [String] the header of field
# @param format [String] the format of field for date field
# @param helper_method [Symbol] the helper method for field, should be defined in custom_helper.rb file, will take two arguments, +record+ and +field_name+
# @param height [Integer] the height of field for image field
# @param width [Integer] the width of field for image field
# @params custom_link [String] the custom link for field
# @params tag_class [Hash] the tag class for field, example: { approved: 'completed', draft: 'active-two', rejected: 'danger' }, this will add css class to tag
# @params display_if [Proc] A lambda that takes the current object and return true or false
# @example Creating a column
# column('name', field_type: :string)
def column(field_name, options = {})
@available_fields[@current_action.name.to_sym] ||= []
raise 'Only one column can be locked in a table.' if @available_fields[@current_action.name.to_sym].select { |x| x.lockable }.size > 0 && options[:lockable]
duplicate_columns = @available_fields[@current_action.name.to_sym].filter { |x| x.field_name.to_sym == field_name }
terminate = false
if duplicate_columns.size.positive?
duplicate_columns.each do |column|
if options[:field_type].to_s != 'association'
terminate = true
elsif options[:field_type].to_s == 'association' && column.association_name.to_s == options[:association_name].to_s
terminate = true
end
end
end
return if terminate
@available_fields[@current_action.name.to_sym] << CmAdmin::Models::Column.new(field_name, options)
end
# Get all columns for a model for index layout.
# @param exclude [Array] the array of fields to exclude
# @example Getting all columns
# all_db_columns(exclude: ['id'])
def all_db_columns(options = {})
field_names = instance_variable_get(:@ar_model)&.columns&.map { |x| x.name.to_sym }
if options.include?(:exclude) && field_names
excluded_fields = ([] << options[:exclude]).flatten.map(&:to_sym)
field_names -= excluded_fields
end
field_names.each do |field_name|
column field_name
end
end
# Create a new custom action for model
# @param name [String] the name of action
# @param page_title [String] the title of page
# @param page_description [String] the description of page
# @param display_name [String] the display name of action
# @param verb [String] the verb of action, +get+, +post+, +put+, +patch+ or +delete+
# @param layout [String] the layout of action
# @param layout_type [String] the layout type of action, +cm_association_index+, +cm_association_show+
# @param partial [String] the partial path of action
# @param path [String] the path of action
# @param display_type [Symbol] the display type of action, +:button+, +:modal+
# @param modal_configuration [Hash] the configuration of modal
# @param url_params [Hash] the url params of action
# @param display_if [Proc] A lambda that takes the current object and return true or false
# @param route_type [String] the route type of action, +member+, +collection+
# @param icon_name [String] the icon name of action, follow font-awesome icon name
# @example Creating a custom action with modal
# custom_action name: 'approve', route_type: 'member', verb: 'patch', icon_name: 'fa-regular fa-circle-check', path: ':id/approve', display_type: :modal, display_if: lambda(&:draft?), modal_configuration: { title: 'Approve Post', description: 'Are you sure you want approve this post', confirmation_text: 'Approve' } do
# post = ::Post.find(params[:id])
# post.approved!
# post
# end
# @example Creating a custom action with button
# custom_action name: 'approve', route_type: 'member', verb: 'patch', icon_name: 'fa-regular fa-circle-check', path: ':id/approve', display_type: :button, display_if: lambda(&:draft?) do
# post = ::Post.find(params[:id])
# post.approved!
# post
# end
def custom_action(name: nil, page_title: nil, page_description: nil, display_name: nil, verb: nil, layout: nil, layout_type: nil, partial: nil, path: nil, display_type: nil, modal_configuration: {}, url_params: {}, display_if: ->(_arg) { true }, route_type: nil, icon_name: 'fa fa-th-large', &block)
action = CmAdmin::Models::CustomAction.new(
page_title:, page_description:,
name:, display_name:, verb:, layout:,
layout_type:, partial:, path:,
parent: current_action.name, display_type:, display_if:,
action_type: :custom, route_type:, icon_name:, modal_configuration:,
model_name: self.name, url_params:, &block
)
@available_actions << action
end
# Create a new bulk action for model
# @param name [String] the name of action
# @param display_name [String] the display name of action
# @param display_if [Proc] A lambda that takes the current object and return true or false
# @param redirection_url [String] the redirection url of action
# @param icon_name [String] the icon name of action, follow font-awesome icon name
# @param verb [String] the verb of action, +get+, +post+, +put+, +patch+ or +delete+
# @param display_type [Symbol] the display type of action, +:page+, +:modal+, +:button+, +:icon_only+[deprecated]
# @param modal_configuration [Hash] the configuration of modal
# @param route_type [String] the route type of action, +member+, +collection+
# @param partial [String] the partial path of action
# @param execution_mode [Symbol] the types of execution mode are +:bulk+, +:individual+ [default +:individual+]
# @example Creating a bulk action
# bulk_action name: 'approve', display_name: 'Approve', display_if: lambda { |arg| arg.draft? }, redirection_url: '/posts', icon_name: 'fa-regular fa-circle-check', verb: :patch, display_type: :modal, modal_configuration: { title: 'Approve Post', description: 'Are you sure you want approve this post', confirmation_text: 'Approve' }, execution_mode: :individual do
# posts = ::Post.where(id: params[:ids])
# posts.each(&:approved!)
# posts
# end
def bulk_action(name: nil, display_name: nil, display_if: ->(_arg) { true }, redirection_url: nil, icon_name: nil, verb: nil, display_type: nil, modal_configuration: {}, route_type: nil, partial: nil, execution_mode: :individual, &block)
bulk_action = CmAdmin::Models::BulkAction.new(
name:, display_name:, display_if:, modal_configuration:,
redirection_url:, icon_name:, action_type: :bulk_action,
execution_mode:,
verb:, display_type:, route_type:, partial:, &block
)
@available_actions << bulk_action
end
# Create a new filter for model
# @param db_column_name [String] the name of column
# @param filter_type [String] the type of filter, +:date+, +:multi_select+, +:range+, +:search+, +:single_select+
# @param placeholder [String] the placeholder of filter
# @param helper_method [String] the helper method for filter, should be defined in custom_helper.rb file
# @param filter_with [Symbol] filter with scope name on model
# @param active_by_default [Boolean] make filter active by default
# @param collection [Array] the collection of filter, use with single_select or multi_select
# @example Creating a filter
# filter('name', :search)
# filter('created_at', :date)
# filter('status', :single_select, collection: ['draft', 'published'])
# filter('status', :multi_select, helper_method: 'status_collection')
# filter('age', :range)
def filter(db_column_name, filter_type, options = {})
@filters << CmAdmin::Models::Filter.new(db_column_name:, filter_type:, options:)
end
# @deprecated: use {#sortable_columns} instead of this method
# Set sort direction for filters
# @param direction [Symbol] the direction of sort, +:asc+, +:desc+
# @example Setting sort direction
# sort_direction(:asc)
def sort_direction(direction = :desc)
raise ArgumentError, "Select a valid sort direction like #{CmAdmin::Models::Action::VALID_SORT_DIRECTION.join(' or ')} instead of #{direction}" unless CmAdmin::Models::Action::VALID_SORT_DIRECTION.include?(direction.to_sym.downcase)
@current_action.sort_direction = direction.to_sym if @current_action
end
# @deprecated: use {#sortable_columns} instead of this method
# Set sort column for filters
# @param column [Symbol] the column name
# @example Setting sort column
# sort_column(:created_at)
def sort_column(column = :created_at)
@current_action.sort_column = column.to_sym if @current_action
end
# Adds a new alert to the current section.
#
# @param [String, nil] header The title text for the alert.
# @param [String, nil] body A string to display as the body of the alert.
# @param [Symbol, nil] type The type of alert. Accepts one of the following symbols: :info, :success, :danger, :warning.
# @param [String, nil] partial The path to a custom partial or HTML for the alert.
# @param [Proc, nil] display_if A lambda function that determines whether the alert should be shown. Should return a boolean.
# @param [Hash] html_attrs Additional HTML attributes to apply to the alert. Has no effect on partials.
#
# @example Basic info alert
# alert_box(header: "Information", body: "This is an informational message.", type: :info)
#
# @example Basic info alert with custom body
# alert_box(header: "Information", body: "This is an informational message. <br>This is a break text", type: :info)
#
# @example Alert with custom partial
# alert_box(partial: "/users/sessions/alert", display_if: ->(arg) { arg.present? })
#
# @example Alert with conditional display and custom HTML attributes
# alert_box(
# header: "Warning",
# body: "Please review your submission.",
# type: :warning,
# display_if: ->(user) { user.submissions.any?(&:incomplete?) },
# html_attrs: { id: "submission-warning", data: { turbo_frame: "warnings" } }
# )
#
# @note Alerts cannot be placed inside nested sections. If added within a nested section,
# the alert will appear on the wrapped cm_show_section.
# @note Only the specified types (info, success, danger, warning) are supported.
# Any other type will default to a standard div.
#
# @see file:docs/AddingAlert.md For more information on how to add alerts to your model.
#
def alert_box(header: nil, body: nil, type: nil, partial: nil, display_if: nil, html_attrs: {})
@section_fields << CmAdmin::Models::Alert.new(header, body, type, partial:, display_if:, html_attrs:)
end
# Configure sortable columns for model
# @note default sort direction will be ascending
# @param columns [Array] the array of hash for column and display name
# @example Sortable Columns
# sortable_columns([{column: 'id', display_name: 'ID'},
# {column: 'updated_at', display_name: 'Last Updated At'}])
#
# @example Sortable Columns with default column and direction
# sortable_columns([{column: 'id', display_name: 'ID', default: true, default_direction: 'desc'},
# {column: 'updated_at', display_name: 'Last Updated At'}])
def sortable_columns(columns)
@sort_columns = columns
default_column = columns.filter do |column|
column.key?(:default) || column.key?(:default_direction)
end
raise ArgumentError, 'only one column can be default' if default_column.size > 1
return if default_column.blank?
default_column = default_column.first
@default_sort_column = default_column[:column]
@default_sort_direction = default_column[:default_direction] if default_column[:default_direction].present?
end
end
end
end