adminable 0.0.1 → 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +94 -33
  3. data/app/assets/javascripts/adminable/scripts.js +8 -5
  4. data/app/assets/stylesheets/adminable/application.scss +15 -0
  5. data/app/controllers/adminable/application_controller.rb +0 -3
  6. data/app/controllers/adminable/resources_controller.rb +49 -24
  7. data/app/models/concerns/adminable/resource_concern.rb +15 -0
  8. data/app/views/adminable/resources/_form.html.haml +5 -9
  9. data/app/views/adminable/resources/_search.html.haml +2 -2
  10. data/app/views/adminable/resources/edit.html.haml +1 -1
  11. data/app/views/adminable/resources/form/_belongs_to.html.haml +13 -3
  12. data/app/views/adminable/resources/form/_boolean.html.haml +5 -2
  13. data/app/views/adminable/resources/form/_date.html.haml +3 -0
  14. data/app/views/adminable/resources/form/_datetime.html.haml +3 -2
  15. data/app/views/adminable/resources/form/_decimal.html.haml +3 -2
  16. data/app/views/adminable/resources/form/_float.html.haml +3 -2
  17. data/app/views/adminable/resources/form/_has_many.html.haml +12 -8
  18. data/app/views/adminable/resources/form/_integer.html.haml +3 -2
  19. data/app/views/adminable/resources/form/_string.html.haml +3 -2
  20. data/app/views/adminable/resources/form/_text.html.haml +4 -2
  21. data/app/views/adminable/resources/form/_time.html.haml +3 -0
  22. data/app/views/adminable/resources/form/_timestamp.html.haml +3 -0
  23. data/app/views/adminable/resources/index.html.haml +12 -14
  24. data/app/views/adminable/resources/index/_belongs_to.html.haml +4 -5
  25. data/app/views/adminable/resources/index/_date.html.haml +2 -0
  26. data/app/views/adminable/resources/index/_has_many.html.haml +1 -2
  27. data/app/views/adminable/resources/index/_text.html.haml +1 -1
  28. data/app/views/adminable/resources/index/_time.html.haml +2 -0
  29. data/app/views/adminable/resources/index/_timestamp.html.haml +2 -0
  30. data/app/views/adminable/shared/_label.html.haml +6 -3
  31. data/app/views/layouts/adminable/application.html.haml +3 -4
  32. data/config/locales/adminable.en.yml +4 -1
  33. data/config/locales/adminable.ru.yml +21 -0
  34. data/config/routes.rb +1 -1
  35. data/lib/adminable.rb +9 -4
  36. data/lib/adminable/attributes/association.rb +7 -14
  37. data/lib/adminable/attributes/base.rb +36 -18
  38. data/lib/adminable/attributes/collection.rb +75 -36
  39. data/lib/adminable/attributes/types/belongs_to.rb +1 -3
  40. data/lib/adminable/attributes/types/date.rb +8 -0
  41. data/lib/adminable/attributes/types/has_many.rb +0 -6
  42. data/lib/adminable/attributes/types/time.rb +8 -0
  43. data/lib/adminable/attributes/types/timestamp.rb +8 -0
  44. data/lib/adminable/configuration.rb +7 -2
  45. data/lib/adminable/engine.rb +5 -1
  46. data/lib/adminable/errors.rb +7 -0
  47. data/lib/adminable/presenters/base_presenter.rb +11 -0
  48. data/lib/adminable/presenters/entries_presenter.rb +55 -0
  49. data/lib/adminable/presenters/entry_presenter.rb +68 -0
  50. data/lib/adminable/resource.rb +9 -15
  51. data/lib/adminable/version.rb +1 -1
  52. data/lib/generators/adminable/install/templates/application_controller.rb +2 -0
  53. data/lib/generators/adminable/install_generator.rb +27 -0
  54. data/lib/generators/{cms → adminable}/resource/templates/resource_controller.rb.erb +0 -0
  55. data/lib/generators/{cms → adminable}/resource_generator.rb +0 -0
  56. metadata +77 -33
  57. data/lib/adminable/extensions/devise.rb +0 -24
@@ -1,2 +1,3 @@
1
- = render 'adminable/shared/label', f: f, attribute: attribute
2
- = f.number_field attribute.name, class: 'form-control'
1
+ = render 'adminable/shared/label', attribute: attribute
2
+ = number_field_tag "#{@resource.model.model_name.param_key}[#{attribute.name}]",
3
+ entry[attribute.name], class: 'form-control'
@@ -1,2 +1,3 @@
1
- = render 'adminable/shared/label', f: f, attribute: attribute
2
- = f.number_field attribute.name, class: 'form-control'
1
+ = render 'adminable/shared/label', attribute: attribute
2
+ = number_field_tag "#{@resource.model.model_name.param_key}[#{attribute.name}]",
3
+ entry[attribute.name], class: 'form-control'
@@ -1,9 +1,13 @@
1
- = render 'adminable/shared/label', f: f, attribute: attribute
1
+ = render 'adminable/shared/label', attribute: attribute
2
2
  = hidden_field_tag "#{@resource.model.model_name.param_key}[#{attribute.key}][]", nil
3
- #clusterizeScrollArea.clusterize-scroll
4
- #clusterizeContentArea.clusterize-content.list-group
5
- - attribute.options_for_select(entry).each do |value, key|
6
- .list-group-item.bg-faded.toggleCheckbox{ :class => ('active' if f.object.send(attribute.key).include?(key)) }
7
- = check_box_tag "#{@resource.model.model_name.param_key}[#{attribute.key}][]",
8
- key, f.object.send(attribute.key).include?(key), hidden: true
9
- = value
3
+ #clusterizeScrollArea.clusterize-scroll.associations
4
+ #clusterizeContentArea.clusterize-content
5
+ - attribute.association.all.each do |association_entry|
6
+ .association
7
+ %label.c-input.c-checkbox.m-a-0
8
+ = check_box_tag "#{@resource.model.model_name.param_key}[#{attribute.key}][]",
9
+ association_entry.id, entry.send(attribute.key).include?(association_entry.id)
10
+ %span.c-indicator
11
+ = link_to association_entry.to_name,
12
+ edit_polymorphic_path(association_entry),
13
+ target: '_blank'
@@ -1,2 +1,3 @@
1
- = render 'adminable/shared/label', f: f, attribute: attribute
2
- = f.number_field attribute.name, class: 'form-control'
1
+ = render 'adminable/shared/label', attribute: attribute
2
+ = number_field_tag "#{@resource.model.model_name.param_key}[#{attribute.name}]",
3
+ entry[attribute.name], class: 'form-control'
@@ -1,2 +1,3 @@
1
- = render 'adminable/shared/label', f: f, attribute: attribute
2
- = f.text_field attribute.name, class: 'form-control'
1
+ = render 'adminable/shared/label', attribute: attribute
2
+ = text_field_tag "#{@resource.model.model_name.param_key}[#{attribute.name}]",
3
+ entry[attribute.name], class: 'form-control'
@@ -1,2 +1,4 @@
1
- = render 'adminable/shared/label', f: f, attribute: attribute
2
- = f.text_area attribute.name, class: "form-control #{ 'wysiwyg' if attribute.wysiwyg? }", rows: 5
1
+ = render 'adminable/shared/label', attribute: attribute
2
+ = text_area_tag "#{@resource.model.model_name.param_key}[#{attribute.name}]",
3
+ entry[attribute.name],
4
+ class: "form-control #{ 'wysiwyg' if attribute.options[:wysiwyg] }", rows: 5
@@ -0,0 +1,3 @@
1
+ = render 'adminable/shared/label', attribute: attribute
2
+ = text_field_tag "#{@resource.model.model_name.param_key}[#{attribute.name}]",
3
+ entry[attribute.name], class: 'form-control'
@@ -0,0 +1,3 @@
1
+ = render 'adminable/shared/label', attribute: attribute
2
+ = text_field_tag "#{@resource.model.model_name.param_key}[#{attribute.name}]",
3
+ entry[attribute.name], class: 'form-control'
@@ -1,35 +1,33 @@
1
1
  .m-b-1.clearfix
2
2
  %h1.pull-md-left.m-b-2
3
- = @resource.model.model_name.human.pluralize
3
+ = @resource.model.model_name.human(count: 2)
4
4
  = link_to t('adminable.buttons.new', resource: @resource.model.model_name.human),
5
5
  new_polymorphic_path(@resource.model), class: 'btn btn-primary pull-md-right m-b-2'
6
6
  .row
7
- .col-md-9
7
+ %div{ :class => (@resource.attributes.search.any? ? 'col-md-9' : 'col-md-12') }
8
8
  .table-responsive.m-b-3
9
9
  %table.table.table-striped
10
10
  %thead
11
11
  %tr
12
- - @resource.attributes.index.each_value do |attribute|
13
- %th.text-nowrap{ :class => ('text-md-center' if attribute.center?) }
14
- = @resource.model.human_attribute_name(attribute.name)
12
+ - @resource.attributes.index.each do |attribute|
13
+ %th.text-nowrap{ :class => ('text-md-center' if attribute.options[:center]) }
14
+ = sort_link(@q, attribute.name, hide_indicator: true)
15
15
  %th
16
16
  %tbody
17
17
  - @entries.each do |entry|
18
18
  %tr
19
- - @resource.attributes.index.each_value do |attribute|
20
- %td{ :class => ('text-md-center' if attribute.center?) }
19
+ - @resource.attributes.index.each do |attribute|
20
+ %td{ :class => ('text-md-center' if attribute.options[:center]) }
21
21
  = render attribute.index_partial_path, entry: entry,
22
22
  attribute: attribute, value: entry[attribute.key]
23
23
  %td.text-nowrap.text-md-right
24
- = link_to t('adminable.buttons.edit'), edit_polymorphic_path(entry),
25
- class: 'label label-primary'
26
- = link_to t('adminable.buttons.delete'), polymorphic_path(entry),
27
- class: 'label label-danger', method: :delete,
28
- data: { confirm: t('adminable.ui.confirm') }
24
+ = entry.link_to_edit_small
25
+ = entry.link_to_delete_small
29
26
  - if @entries.empty?
30
27
  %tr
31
28
  %td.text-md-center.text-muted{ :colspan => @resource.attributes.index.size + 1 }
32
29
  No data available
33
- .col-md-3.hidden-sm-down
34
- = render 'search'
30
+ - if @resource.attributes.search.any?
31
+ .col-md-3.hidden-sm-down
32
+ = render 'search'
35
33
  = paginate @entries, views_prefix: 'adminable'
@@ -1,5 +1,4 @@
1
- - if value.present?
2
- - if attribute.association.klass.exists?(value)
3
- = link_to "##{ value }", edit_polymorphic_path(attribute.association.klass.find_by(id: value))
4
- - else
5
- = "##{ value }"
1
+ - if entry.public_send(attribute.name)
2
+ = Adminable::EntryPresenter.new(entry.public_send(attribute.name)).link_to_self
3
+ - else
4
+ = "##{value}"
@@ -0,0 +1,2 @@
1
+ - if value
2
+ = l value, format: '%-d %b %Y'
@@ -1,3 +1,2 @@
1
1
  - if entry
2
- - entry.send(attribute.name).each do |resource|
3
- = link_to "##{ resource.id }", edit_polymorphic_path(resource)
2
+ = Adminable::EntriesPresenter.new(entry.public_send(attribute.name)).to_s
@@ -1 +1 @@
1
- = strip_tags(value.truncate(100)).squish
1
+ = strip_tags(value).truncate(30).squish
@@ -0,0 +1,2 @@
1
+ - if value
2
+ = l value, format: '%H:%M:%S'
@@ -0,0 +1,2 @@
1
+ - if value
2
+ = l value, format: '%H:%M:%S, %-d %b %Y'
@@ -1,5 +1,8 @@
1
1
  = label_tag attribute.name do
2
2
  = @resource.model.human_attribute_name(attribute.name)
3
- - if attribute.required?
4
- %i.text-muted
5
- (required)
3
+ - if attribute.options[:required]
4
+ %span.text-muted.m-l-1
5
+ %i.fa.fa-exclamation-circle{ 'data-toggle' => :tooltip, 'data-placement' => :right, :title => 'Required' }
6
+ - if %i(belongs_to has_many).include?(attribute.type)
7
+ %span.text-muted.m-l-1.uncheck-associations
8
+ %i.fa.fa-remove{ 'data-toggle' => :tooltip, 'data-placement' => :right, :title => 'Uncheck all' }
@@ -14,7 +14,7 @@
14
14
  %body
15
15
  #collapsing-nav.collapse.p-y-1
16
16
  - Adminable::Configuration.resources.each do |resource|
17
- = link_to resource.model.model_name.human.pluralize,
17
+ = link_to resource.model.model_name.human(count: 2),
18
18
  polymorphic_path(resource.route), class: 'btn btn-link btn-block'
19
19
  .navbar.navbar-full.navbar-dark.bg-primary.m-b-3
20
20
  .hidden-md-up
@@ -27,14 +27,13 @@
27
27
  %i.fa.fa-home
28
28
  - Adminable::Configuration.resources.each do |resource|
29
29
  %li.nav-item{ :class => ('active' if current_page?(File.join('/adminable/', resource.name))) }
30
- = link_to resource.model.model_name.human.pluralize,
30
+ = link_to resource.model.model_name.human(count: 2),
31
31
  polymorphic_path(resource.route), class: 'nav-link'
32
32
  .container.p-b-3
33
33
  - if flash[:alert] || notice.present?
34
34
  .p-b-2
35
35
  - if flash[:alert]
36
- - flash[:alert].each do |alert|
37
- = render 'adminable/shared/alert', message: alert, type: 'danger'
36
+ = render 'adminable/shared/alert', message: alert, type: 'danger'
38
37
  - if notice.present?
39
38
  = render 'adminable/shared/alert', message: notice, type: 'success'
40
39
  = yield
@@ -4,13 +4,16 @@ en:
4
4
  ui:
5
5
  confirm: Are you sure?
6
6
  not_selected: Not selected
7
+ and_more: and %{size} more.
7
8
  buttons:
8
9
  submit: Submit
9
10
  back: Back
10
11
  edit: Edit
12
+ delete: Delete
11
13
  new: New %{resource}
14
+ search: Search
12
15
  titles:
13
- edit: Edit %{resource}
16
+ edit: "Edit %{resource} #%{id}"
14
17
  new: New %{resource}
15
18
  resources:
16
19
  created: "%{resource} has been successfully created."
@@ -0,0 +1,21 @@
1
+ ---
2
+ ru:
3
+ adminable:
4
+ ui:
5
+ confirm: Вы уверены?
6
+ not_selected: Не выбрано
7
+ and_more: и еще %{size}.
8
+ buttons:
9
+ submit: Сохранить
10
+ back: Назад
11
+ edit: Изменить
12
+ delete: Удалить
13
+ new: Новый %{resource}
14
+ search: Поиск
15
+ titles:
16
+ edit: "Редактирование %{resource} #%{id}"
17
+ new: Создание %{resource}
18
+ resources:
19
+ created: "%{resource} успешно создан."
20
+ updated: "%{resource} успешно обновлен."
21
+ deleted: "%{resource} успешно удален."
@@ -3,5 +3,5 @@ Adminable::Engine.routes.draw do
3
3
  resources r.route, except: :show, path: r.name, controller: r.name
4
4
  end
5
5
 
6
- root 'application#welcome'
6
+ root to: redirect(Adminable::Configuration.resources.first.name)
7
7
  end
@@ -1,10 +1,13 @@
1
1
  require 'adminable/engine'
2
2
  require 'adminable/configuration'
3
-
4
- require 'adminable/extensions/devise'
3
+ require 'adminable/errors'
5
4
 
6
5
  require 'adminable/resource'
7
6
 
7
+ require 'adminable/presenters/base_presenter'
8
+ require 'adminable/presenters/entry_presenter'
9
+ require 'adminable/presenters/entries_presenter'
10
+
8
11
  require 'adminable/attributes/collection'
9
12
  require 'adminable/attributes/association'
10
13
 
@@ -12,6 +15,7 @@ require 'adminable/attributes/base'
12
15
 
13
16
  require 'adminable/attributes/types/belongs_to'
14
17
  require 'adminable/attributes/types/boolean'
18
+ require 'adminable/attributes/types/date'
15
19
  require 'adminable/attributes/types/datetime'
16
20
  require 'adminable/attributes/types/decimal'
17
21
  require 'adminable/attributes/types/float'
@@ -19,15 +23,16 @@ require 'adminable/attributes/types/has_many'
19
23
  require 'adminable/attributes/types/integer'
20
24
  require 'adminable/attributes/types/string'
21
25
  require 'adminable/attributes/types/text'
26
+ require 'adminable/attributes/types/time'
27
+ require 'adminable/attributes/types/timestamp'
22
28
 
23
29
  require 'haml-rails'
24
30
  require 'sass-rails'
25
- require 'coffee-rails'
26
31
  require 'jquery-rails'
27
32
  require 'bootstrap'
28
33
  require 'rails-assets-tether'
29
- require 'babosa'
30
34
  require 'kaminari'
35
+ require 'ransack'
31
36
 
32
37
  module Adminable
33
38
  end
@@ -1,21 +1,14 @@
1
1
  module Adminable
2
2
  module Attributes
3
- module Association
4
- attr_reader :options_for_select, :association
3
+ class Association
4
+ attr_reader :reflection, :model, :all
5
5
 
6
- def options_for_select(entry)
7
- @association.klass.all.reject { |e| e == entry }.collect do |e|
8
- [association_option_name(e), e.id]
9
- end
6
+ # @param reflection [Object] ActiveRecord::Reflection::HasManyReflection
7
+ def initialize(reflection)
8
+ @reflection = reflection
9
+ @model = @reflection.klass
10
+ @all = Adminable::EntriesPresenter.new(@model)
10
11
  end
11
-
12
- private
13
-
14
- def association_option_name(entry)
15
- %i(email login name title id).each do |a|
16
- return entry.try(a) unless entry.try(a).nil?
17
- end
18
- end
19
12
  end
20
13
  end
21
14
  end
@@ -1,33 +1,38 @@
1
1
  module Adminable
2
2
  module Attributes
3
+ # Base class for attributes types
4
+ # @note Cannot be initialized
3
5
  class Base
4
- attr_accessor :name, :required, :show, :center, :wysiwyg
5
- attr_reader :key, :ransack_name
6
+ attr_reader :name, :options, :key, :strong_parameter, :association
6
7
 
8
+ # @param name [Symbol] attribute name e.g. `:id` or `:title`
9
+ # @param options [Hash] options, see {default_options}
7
10
  def initialize(name, options = {})
8
11
  raise 'Base class cannot be initialized' if self.class == Base
9
12
 
10
- @name = name
11
- @key = options[:key] || @name
12
- @ransack_name = "#{@name}_cont"
13
- @association = options[:association]
14
- @required = options[:required] || false
15
- @show = true
16
- @center = %w(integer boolean float decimal).include?(type)
17
- @wysiwyg = %w(text).include?(type)
18
- end
13
+ @name = name.to_sym
14
+ @options = default_options.merge(options)
19
15
 
20
- alias required? required
21
- alias show? show
22
- alias center? center
23
- alias wysiwyg? wysiwyg
16
+ @association = Adminable::Attributes::Association.new(
17
+ options[:association]
18
+ ) if options[:association]
19
+ end
24
20
 
25
- def type
26
- self.class.name.demodulize.underscore
21
+ def key
22
+ @key ||= name
27
23
  end
28
24
 
29
25
  def strong_parameter
30
- key
26
+ @strong_parameter ||= key
27
+ end
28
+
29
+ def ransack_name
30
+ @ransack_name ||= "#{name}_cont"
31
+ end
32
+
33
+ # @return [Symbol] class type e.g. `:text` for Adminable::Attributes::Text
34
+ def type
35
+ @type ||= self.class.name.demodulize.underscore.to_sym
31
36
  end
32
37
 
33
38
  def index_partial_path
@@ -37,6 +42,19 @@ module Adminable
37
42
  def form_partial_path
38
43
  "adminable/resources/form/#{type}"
39
44
  end
45
+
46
+ private
47
+
48
+ def default_options
49
+ {
50
+ index: %i(belongs_to has_many).exclude?(type),
51
+ search: false,
52
+ form: %i(id created_at updated_at).exclude?(name),
53
+ required: false,
54
+ center: %i(integer boolean float decimal).include?(type),
55
+ wysiwyg: false
56
+ }
57
+ end
40
58
  end
41
59
  end
42
60
  end
@@ -1,73 +1,112 @@
1
1
  module Adminable
2
2
  module Attributes
3
3
  class Collection
4
- attr_reader :model
5
- attr_writer :index, :form
4
+ include Enumerable
5
+ extend Forwardable
6
6
 
7
+ def_delegators :@all, :[], :<<, :each, :first, :last, :push, :unshift
8
+
9
+ attr_reader :model, :all
10
+
11
+ # @param model [Class] model from Rails application e.g. `User` or `Post`
7
12
  def initialize(model)
8
13
  @model = model
14
+ @all ||= columns + associations
9
15
  end
10
16
 
11
17
  def index
12
- @index ||= all.except(:created_at, :updated_at)
18
+ all.select { |attribute| attribute.options[:index] }
13
19
  end
14
20
 
15
21
  def form
16
- @form ||= all.except(:id, :created_at, :updated_at)
17
- end
18
-
19
- def ransack
20
- @ransack ||= columns.except(:id, :created_at, :updated_at)
21
- .select { |k, v| %w(string text).include?(v.type) }
22
- end
23
-
24
- def all
25
- [columns, belongs_to, has_many].inject(&:merge)
22
+ all.select { |attribute| attribute.options[:form] }
26
23
  end
27
24
 
28
- def association
29
- [belongs_to, has_many].inject(&:merge)
25
+ def search
26
+ all.select { |attribute| attribute.options[:search] }
30
27
  end
31
28
 
29
+ # Collects attributes from model columns
30
+ # @return [Array]
31
+ #
32
+ # rubocop:disable Metrics/MethodLength
32
33
  def columns
33
- @columns ||= {}.tap do |attribute|
34
+ @columns ||= [].tap do |attributes|
34
35
  @model.columns.reject { |a| a.name.match(/_id$/) }.each do |column|
35
- attribute[column.name] = "adminable/attributes/types/#{column.type}"
36
- .classify.constantize.new(
36
+ begin
37
+ attributes << resolve(column.type).new(
37
38
  column.name,
38
- required: attribute_required?(column.name)
39
+ required: required?(column.name)
39
40
  )
41
+ rescue Adminable::AttributeNotImplemented
42
+ next
43
+ end
40
44
  end
41
- end.symbolize_keys
45
+ end
42
46
  end
43
47
 
44
- def belongs_to
45
- @belongs_to ||= {}.tap do |attribute|
46
- @model.reflect_on_all_associations(:belongs_to).each do |association|
47
- attribute[association.name] = Adminable::Attributes::Types::BelongsTo.new(
48
+ # Collects attributes from model associations
49
+ # @return [Array]
50
+ def associations
51
+ @associations ||= [].tap do |attributes|
52
+ @model.reflect_on_all_associations.each do |association|
53
+ attributes << resolve(association.macro).new(
48
54
  association.name,
49
- required: attribute_required?(association.name),
55
+ required: required?(association.name),
50
56
  association: association
51
57
  )
52
58
  end
53
- end.symbolize_keys
59
+ end
54
60
  end
55
61
 
56
- def has_many
57
- @has_many ||= {}.tap do |attribute|
58
- @model.reflect_on_all_associations(:has_many).each do |association|
59
- attribute[association.name] = Adminable::Attributes::Types::HasMany.new(
60
- association.name,
61
- required: attribute_required?(association.name),
62
- association: association
63
- )
62
+ def configure
63
+ yield
64
+ end
65
+
66
+ # Changes options for given attribute
67
+ # @param name [Symbol] name of attribute e.g. `:title`
68
+ # @param options [Hash] options to update
69
+ def set(*args)
70
+ options = args.extract_options!
71
+ names = args
72
+
73
+ options.each do |key, value|
74
+ names.each do |name|
75
+ get(name).options[key] = value
64
76
  end
65
- end.symbolize_keys
77
+ end
78
+ end
79
+
80
+ # Finds attribute by name
81
+ # @param name [Symbol] name of attribute e.g. `:title`
82
+ # @return [Object] e.g. `Adminable::Attribute::Types::String` for `:title`
83
+ def get(name)
84
+ @all.find { |attribute| attribute.name == name } ||
85
+ raise(
86
+ Adminable::AttributeNotFound,
87
+ "couldn't find attribute with name `#{name}`"
88
+ )
89
+ end
90
+
91
+ # Adds new attribute to collection
92
+ # @param name [Symbol] name of attribute e.g. `:title`
93
+ # @param type [Symbol] type of attribute e.g. `:string`
94
+ def add(name, type, options = {})
95
+ @all << resolve(type).new(name, **options)
66
96
  end
67
97
 
68
98
  private
69
99
 
70
- def attribute_required?(name)
100
+ def resolve(type)
101
+ "adminable/attributes/types/#{type}".classify.constantize
102
+ rescue NameError
103
+ raise(
104
+ Adminable::AttributeNotImplemented,
105
+ "type `#{type}` is not supported yet."
106
+ )
107
+ end
108
+
109
+ def required?(name)
71
110
  @model.validators_on(name).any? do |validator|
72
111
  validator.class == ActiveRecord::Validations::PresenceValidator
73
112
  end