require "netzke/grid_panel/grid_panel_js"
require "netzke/grid_panel/grid_panel_api"
require "netzke/plugins/configuration_tool"
require "netzke/data_accessor"
module Netzke
# == GridPanel
# Ext.grid.EditorGridPanel + server-side code
#
# == Features:
# * multi-line CRUD operations - get, post, delete, create
# * (multe-record) editing and adding records through a form
# * column resize, move and hide
# * permissions
# * sorting
# * pagination
# * filtering
# * extended configurable search
# * rows reordering (drag-n-drop)
# * dynamic configuration of properties and columns
#
# == Class configuration
# Configuration on this level is effective during the life-time of the application. They can be put into a .rb file
# inside of config/initializers like this:
#
# Netzke::GridPanel.configure :column_filters_available, false
# Netzke::GridPanel.configure :default_config => {:ext_config => {:enable_config_tool => false}}
#
# Most of these options directly influence the amount of JavaScript code that is generated for this widget's class.
# The less functionality is enabled, the less code is generated.
#
# The following configuration options are available:
# * :column_filters_available - (default is true) include code for the filters in the column's context menu
# * :config_tool_available - (default is true) include code for the configuration tool that launches the configuration panel
# * :edit_in_form_available - (defaults to true) include code for (multi-record) editing and adding records through a form
# * :extended_search_available - (defaults to true) include code for extended configurable search
# * :default_config - a hash of default configuration options for each instance of the GridPanel widget.
# See the "Instance configuration" section below.
#
# == Instance configuration
# The following config options are available:
# * :model - name of the ActiveRecord model that provides data to this GridPanel.
# * :strong_default_attrs - a hash of attributes to be merged atop of every created/updated record.
# * :scopes - an array of named scopes to filter grid data, e.g.:
#
# [["user_id_not", 100], ["name_like", "Peter"]]
#
# In the :ext_config hash (see Netzke::Base) the following GridPanel specific options are available:
#
# * :enable_column_filters - enable filters in column's context menu
# * :enable_edit_in_form - provide buttons into the toolbar that activate editing/adding records via a form
# * :enable_extended_search - provide a button into the toolbar that shows configurable search form
# * :enable_context_menu - enable rows context menu
# * :enable_rows_reordering - enable reordering of rows with drag-n-drop; underlying model (specified in :model) must implement "acts_as_list"-compatible functionality; defaults to false
# * :enable_pagination - enable pagination; defaults to true
# * :rows_per_page - number of rows per page (ignored when :enable_pagination is set to false)
# * :load_inline_data - load initial data into the grid right after its instantiation (saves a request to server); defaults to true
# * :mode - when set to :config, GridPanel loads in configuration mode
#
# Additionally supports Netzke::Base config options.
class GridPanel < Base
# javascript (client-side)
include GridPanelJs
# API (server-side)
include GridPanelApi
# Code shared between GridPanel, FormPanel, and other widgets that serve as interface to database tables
include Netzke::DataAccessor
def self.enforce_config_consistency
config[:default_config][:ext_config][:enable_edit_in_form] &&= config[:edit_in_form_available]
config[:default_config][:ext_config][:enable_extended_search] &&= config[:extended_search_available]
config[:default_config][:ext_config][:enable_rows_reordering] &&= config[:rows_reordering_available]
end
# Class-level configuration. This options directly influence the amount of generated
# javascript code for this widget's class. For example, if you don't want filters for the grid,
# set :column_filters_available to false, and the javascript for the filters won't be included at all.
def self.config
set_default_config({
:column_filters_available => true,
:config_tool_available => true,
:edit_in_form_available => true,
:extended_search_available => true,
:rows_reordering_available => true,
:default_config => {
:ext_config => {
:enable_edit_in_form => true,
:enable_extended_search => true,
:enable_column_filters => true,
:load_inline_data => true,
:enable_context_menu => true,
:enable_rows_reordering => false, # column drag n drop
:enable_pagination => true,
:rows_per_page => 25,
:tools => %w{ refresh },
:mode => :normal # when set to :config, :configuration button is enabled
},
:persistent_config => true
}
})
end
# Include extra javascript that we depend on
def self.include_js
res = []
# Checkcolumn
ext_examples = Netzke::Base.config[:ext_location] + "/examples/"
res << ext_examples + "ux/CheckColumn.js"
# res << "#{File.dirname(__FILE__)}/grid_panel/javascripts/check-column.js"
# Filters
# Not compatible with Ext 3.0
# if config[:column_filters_available]
# ext_examples = Netzke::Base.config[:ext_location] + "/examples/"
# res << ext_examples + "grid-filtering/menu/EditableItem.js"
# res << ext_examples + "grid-filtering/menu/RangeMenu.js"
# res << ext_examples + "grid-filtering/grid/GridFilters.js"
#
# %w{Boolean Date List Numeric String}.unshift("").each do |f|
# res << ext_examples + "grid-filtering/grid/filter/#{f}Filter.js"
# end
#
# res << "#{File.dirname(__FILE__)}/grid_panel/javascripts/filters.js"
#
# end
# DD
if config[:rows_reordering_available]
res << "#{File.dirname(__FILE__)}/grid_panel/javascripts/rows-dd.js"
end
res
end
# Define connection points between client side and server side of GridPanel.
# See implementation of equally named methods in the GridPanelApi module.
api :get_data, :post_data, :delete_data, :resize_column, :move_column, :hide_column, :get_combobox_options, :move_rows
# Edit in form
api :create_new_record if config[:edit_in_form_available]
# (We can't memoize this method because at some point we extend it, e.g. in Netzke::DataAccessor)
def data_class
::ActiveSupport::Deprecation.warn("data_class_name option is deprecated. Use model instead", caller) if config[:data_class_name]
model_name = config[:model] || config[:data_class_name]
@data_class ||= model_name.nil? ? raise(ArgumentError, "No model specified for widget #{global_id}") : model_name.constantize
end
def initialize(config = {}, parent = nil)
super
apply_helpers
end
# Columns to be displayed by the FieldConfigurator.
def self.config_columns
[
{:name => :name, :type => :string, :editor => :combobox, :width => 200},
{:name => :excluded, :type => :boolean, :editor => :checkbox, :width => 40, :header => "Excl"},
{:name => :value},
{:name => :header},
{:name => :hidden, :type => :boolean, :editor => :checkbox},
{:name => :editable, :type => :boolean, :editor => :checkbox, :header => "Editable", :default => true},
{:name => :editor, :type => :string, :editor => {:xtype => :combobox, :options => Netzke::Ext::FORM_FIELD_XTYPES}},
{:name => :renderer, :type => :string},
# maybe later
# {:name => :xtype, :type => :string, :editor => {:xtype => :combobox, :options => Netzke::Ext::COLUMN_XTYPES}},
# {:name => :renderer, :type => :string, :editor => {:xtype => :jsonfield}},
# Filters not supported in Ext 3.0
# {:name => :with_filters, :type => :boolean, :editor => :checkbox, :default => true, :header => "Filters"},
# some rarely used configurations, hidden
{:name => :width, :type => :integer, :editor => :numberfield, :hidden => true},
{:name => :hideable, :type => :boolean, :editor => :checkbox, :default => true, :hidden => true},
{:name => :sortable, :type => :boolean, :editor => :checkbox, :default => true, :hidden => true},
]
end
def self.property_fields
res = [
{:name => :ext_config__title, :type => :string},
{:name => :ext_config__header, :type => :boolean, :default => true},
{:name => :ext_config__enable_context_menu, :type => :boolean, :default => true},
{:name => :ext_config__context_menu, :type => :json},
{:name => :ext_config__enable_pagination, :type => :boolean, :default => true},
{:name => :ext_config__rows_per_page, :type => :integer},
{:name => :ext_config__bbar, :type => :json},
{:name => :ext_config__prohibit_create, :type => :boolean},
{:name => :ext_config__prohibit_update, :type => :boolean},
{:name => :ext_config__prohibit_delete, :type => :boolean},
{:name => :ext_config__prohibit_read, :type => :boolean}
]
res << {:name => :ext_config__enable_extended_search, :type => :boolean} if config[:extended_search_available]
res << {:name => :ext_config__enable_edit_in_form, :type => :boolean} if config[:edit_in_form_available]
# TODO: buggy thing
# res << {:name => :layout__columns, :type => :json}
res
end
def default_config
res = super
res[:ext_config][:bbar] = default_bbar
res[:ext_config][:context_menu] = default_context_menu
res
end
def default_bbar
res = %w{ add edit apply del }
res << "-" << "add_in_form" << "edit_in_form" if self.class.config[:edit_in_form_available]
res << "-" << "search" if self.class.config[:extended_search_available]
res
end
def default_context_menu
res = %w{ edit del }
res << "-" << "edit_in_form" if self.class.config[:edit_in_form_available]
res
end
def configuration_widgets
res = []
res << {
:persistent_config => true,
:name => 'columns',
:widget_class_name => "FieldsConfigurator",
:active => true,
:widget => self
}
res << {
:name => 'general',
:widget_class_name => "PropertyEditor",
:widget => self,
:ext_config => {:title => false}
}
res
end
def actions
# Defaults
{
:add => {:text => 'Add', :disabled => ext_config[:prohibit_create]},
:edit => {:text => 'Edit', :disabled => true},
:del => {:text => 'Delete', :disabled => true},
:apply => {:text => 'Apply', :disabled => ext_config[:prohibit_update] && ext_config[:prohibit_create]},
:add_in_form => {:text => 'Add in form', :disabled => !ext_config[:enable_edit_in_form]},
:edit_in_form => {:text => 'Edit in form', :disabled => true},
:search => {:text => 'Search', :disabled => !ext_config[:enable_extended_search], :checked => true}
}
end
def initial_late_aggregatees
res = {}
# Edit in form
res.merge!({
:add_form => {
:widget_class_name => "GridPanel::RecordFormWindow",
:ext_config => {
:title => "Add #{data_class.name.humanize}",
:button_align => "right"
},
:item => {
:widget_class_name => "FormPanel",
:data_class_name => data_class.name,
:persistent_config => config[:persistent_config],
:strong_default_attrs => config[:strong_default_attrs],
:ext_config => {
:border => true,
:bbar => false,
:header => false,
:mode => ext_config[:mode]
},
:record => data_class.new
}
},
:edit_form => {
:widget_class_name => "FormPanel",
:data_class_name => data_class.name,
:persistent_config => config[:persistent_config],
:ext_config => {
:bbar => false,
:header => false,
:mode => ext_config[:mode]
}
},
:multi_edit_form => {
:widget_class_name => "FormPanel",
:data_class_name => data_class.name,
:persistent_config => config[:persistent_config],
:ext_config => {
:bbar => false,
:header => false,
:mode => ext_config[:mode]
}
},
:new_record_form => {
:widget_class_name => "FormPanel",
:data_class_name => data_class.name,
:persistent_config => config[:persistent_config],
:strong_default_attrs => config[:strong_default_attrs],
:ext_config => {
:bbar => false,
:header => false,
:mode => ext_config[:mode]
},
:record => data_class.new
}
}) if ext_config[:enable_edit_in_form]
# Extended search
res.merge!({
:search_panel => {
:widget_class_name => "SearchPanel",
:search_class_name => data_class.name,
:persistent_config => config[:persistent_config],
:ext_config => {
:header => false,
:bbar => false,
:mode => ext_config[:mode]
},
}
}) if ext_config[:enable_extended_search]
res
end
include Plugins::ConfigurationTool if config[:config_tool_available] # it will load ConfigurationPanel into a modal window
def columns
@columns ||= get_columns
end
# Normalized columns
def normalized_columns
@normalized_columns ||= normalize_columns(columns)
end
def get_columns
if persistent_config_enabled?
columns = persistent_config['layout__columns'] || default_columns
res = normalize_array_of_columns(columns)
else
res = default_columns
end
# denormalize
res.map{ |c| c.is_a?(Hash) && c.reject{ |k,v| k == :name }.empty? ? c[:name].to_sym : c }
end
# Normalizes the column at position +index+ and returns it.
def column_at(index)
if columns[index].is_a?(Hash)
columns[index]
else
column_name = columns.delete_at(index)
normalized_column = normalize_column(column_name)
columns.insert(index, normalized_column)
normalized_column
end
end
# Stores modified columns in persistent storage
def save_columns!
persistent_config[:layout__columns] = columns
end
TYPE_EDITOR_MAP = {
:integer => :numberfield,
:boolean => :checkbox,
:date => :datefield,
:datetime => :xdatetime,
:text => :textarea
# :string => :textfield
}
def default_columns
# columns specified in widget's config
columns_from_config = config[:columns] && normalize_columns(config[:columns])
if columns_from_config
# reverse-merge each column hash from config with each column hash from exposed_attributes (columns from config have higher priority)
for c in columns_from_config
corresponding_exposed_column = predefined_columns.find{ |k| k[:name] == c[:name] }
c.reverse_merge!(corresponding_exposed_column) if corresponding_exposed_column
end
columns_for_create = columns_from_config
else
# we didn't have columns configured in widget's config, so, use the columns from the data class
columns_for_create = predefined_columns
end
columns_for_create.map! do |c|
# detect ActiveRecord column type (if the column is "real") or fall back to :virtual
type = (data_class.columns_hash[c[:name].to_s] && data_class.columns_hash[c[:name].to_s].type) || :virtual
# detect :assoc__method columns
if c[:name].to_s.index('__')
assoc_name, method = c[:name].to_s.split('__').map(&:to_sym)
if assoc = data_class.reflect_on_association(assoc_name)
assoc_column = assoc.klass.columns_hash[method.to_s]
assoc_method_type = assoc_column.try(:type)
if assoc_method_type
c[:editor] ||= TYPE_EDITOR_MAP[assoc_method_type] == :checkbox ? :checkbox : :combobox
end
type = :association
end
end
# detect association column (e.g. :category_id)
assoc = data_class.reflect_on_all_associations.detect{|a| a.primary_key_name.to_sym == c[:name]}
if assoc && !assoc.options[:polymorphic]
c[:editor] ||= :combobox
assoc_method = %w{name title label id}.detect{|m| (assoc.klass.instance_methods + assoc.klass.column_names).include?(m) } || assoc.klass.primary_key
c[:name] = "#{assoc.name}__#{assoc_method}".to_sym
type = :association
end
# Some smart defaults
# default editor, dependent on column type
c[:editor] ||= TYPE_EDITOR_MAP[type] unless TYPE_EDITOR_MAP[type].nil?
# narrow column for checkbox
c[:width] ||= 50 if c[:editor] == :checkbox
# wider column for xdatetime
c[:width] ||= 120 if c[:editor] == :xdatetime
# hide ID column
c[:hidden] = true if c[:name] == data_class.primary_key.to_sym && c[:hidden].nil?
# make ID column read-only
c[:editable] = false if c[:name] == data_class.primary_key.to_sym && c[:editable].nil?
# Some default limitations for virtual columns
if type == :virtual
# disable filters
# c[:with_filters].nil? && c[:with_filters] = false
# disable sorting
c[:sortable].nil? && c[:sortable] = false
# read-only
# c[:read_only].nil? && c[:read_only] = true
c[:editable].nil? && c[:editable] = false
end
# denormalize column (save space)
c.reject{ |k,v| k == :name }.empty? ? c[:name] : c
end
columns_for_create
end
end
end