tiny_admin 0.3.0 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -2,9 +2,11 @@
2
2
 
3
3
  module TinyAdmin
4
4
  class Router < BasicApp
5
- TinyAdmin::Settings.instance.load_settings
6
-
7
5
  route do |r|
6
+ context.settings = TinyAdmin::Settings.instance
7
+ context.settings.load_settings
8
+ context.router = r
9
+
8
10
  r.on 'auth' do
9
11
  context.slug = nil
10
12
  r.run Authentication
@@ -27,11 +29,11 @@ module TinyAdmin
27
29
  r.redirect settings.root_path
28
30
  end
29
31
 
30
- TinyAdmin::Settings.instance.pages.each do |slug, data|
32
+ context.pages.each do |slug, data|
31
33
  setup_page_route(r, slug, data)
32
34
  end
33
35
 
34
- TinyAdmin::Settings.instance.resources.each do |slug, options|
36
+ context.resources.each do |slug, options|
35
37
  setup_resource_routes(r, slug, options: options || {})
36
38
  end
37
39
 
@@ -74,29 +76,30 @@ module TinyAdmin
74
76
  end
75
77
 
76
78
  def setup_collection_routes(router, options:)
77
- repository = options[:repository].new(options[:model])
79
+ context.repository = options[:repository].new(options[:model])
78
80
  action_options = options[:index] || {}
79
81
 
80
82
  # Custom actions
81
83
  custom_actions = setup_custom_actions(
82
84
  router,
83
85
  options[:collection_actions],
84
- repository: repository,
86
+ repository: context.repository,
85
87
  options: action_options
86
88
  )
87
89
 
88
90
  # Index
89
- actions = options[:only]
90
- if !actions || actions.include?(:index) || actions.include?('index')
91
+ if options[:only].include?(:index) || options[:only].include?('index')
91
92
  router.is do
92
- index_action = TinyAdmin::Actions::Index.new(repository, path: request.path, params: request.params)
93
- render_page index_action.call(app: self, context: context, options: action_options, actions: custom_actions)
93
+ context.actions = custom_actions
94
+ context.request = request
95
+ index_action = TinyAdmin::Actions::Index.new
96
+ render_page index_action.call(app: self, context: context, options: action_options)
94
97
  end
95
98
  end
96
99
  end
97
100
 
98
101
  def setup_member_routes(router, options:)
99
- repository = options[:repository].new(options[:model])
102
+ context.repository = options[:repository].new(options[:model])
100
103
  action_options = (options[:show] || {}).merge(record_not_found_page: settings.record_not_found)
101
104
 
102
105
  router.on String do |reference|
@@ -106,32 +109,36 @@ module TinyAdmin
106
109
  custom_actions = setup_custom_actions(
107
110
  router,
108
111
  options[:member_actions],
109
- repository: repository,
112
+ repository: context.repository,
110
113
  options: action_options
111
114
  )
112
115
 
113
116
  # Show
114
- actions = options[:only]
115
- if !actions || actions.include?(:show) || actions.include?('show')
117
+ if options[:only].include?(:show) || options[:only].include?('show')
116
118
  router.is do
117
- show_action = TinyAdmin::Actions::Show.new(repository, path: request.path, params: request.params)
118
- render_page show_action.call(app: self, context: context, options: action_options, actions: custom_actions)
119
+ context.actions = custom_actions
120
+ context.request = request
121
+ show_action = TinyAdmin::Actions::Show.new
122
+ render_page show_action.call(app: self, context: context, options: action_options)
119
123
  end
120
124
  end
121
125
  end
122
126
  end
123
127
 
124
128
  def setup_custom_actions(router, custom_actions, repository:, options:)
125
- (custom_actions || []).map do |custom_action|
129
+ context.repository = repository
130
+ (custom_actions || []).each_with_object({}) do |custom_action, result|
126
131
  action_slug, action = custom_action.first
127
132
  action_class = action.is_a?(String) ? Object.const_get(action) : action
128
133
 
129
134
  router.get action_slug.to_s do
130
- custom_action = action_class.new(repository, path: request.path, params: request.params)
135
+ context.actions = {}
136
+ context.request = request
137
+ custom_action = action_class.new
131
138
  render_page custom_action.call(app: self, context: context, options: options)
132
139
  end
133
140
 
134
- action_slug.to_s
141
+ result[action_slug.to_s] = action_class
135
142
  end
136
143
  end
137
144
  end
@@ -5,10 +5,26 @@ module TinyAdmin
5
5
  include Singleton
6
6
  include Utils
7
7
 
8
+ DEFAULTS = {
9
+ %i[authentication plugin] => Plugins::NoAuth,
10
+ %i[authentication login] => Views::Pages::SimpleAuthLogin,
11
+ %i[components flash] => Views::Components::Flash,
12
+ %i[components head] => Views::Components::Head,
13
+ %i[components navbar] => Views::Components::Navbar,
14
+ %i[components pagination] => Views::Components::Pagination,
15
+ %i[helper_class] => Support,
16
+ %i[page_not_found] => Views::Pages::PageNotFound,
17
+ %i[record_not_found] => Views::Pages::RecordNotFound,
18
+ %i[repository] => Plugins::ActiveRecordRepository,
19
+ %i[root_path] => '/admin',
20
+ %i[root page] => Views::Pages::Root,
21
+ %i[root title] => 'TinyAdmin'
22
+ }.freeze
23
+
8
24
  attr_accessor :authentication,
9
25
  :components,
10
26
  :extra_styles,
11
- :navbar,
27
+ :helper_class,
12
28
  :page_not_found,
13
29
  :record_not_found,
14
30
  :repository,
@@ -18,66 +34,96 @@ module TinyAdmin
18
34
  :scripts,
19
35
  :style_links
20
36
 
21
- attr_reader :pages, :resources
37
+ def [](key)
38
+ send(key)
39
+ end
40
+
41
+ def []=(key, value)
42
+ send("#{key}=", value)
43
+ convert_value(key, value)
44
+ end
22
45
 
23
46
  def load_settings
24
- @authentication ||= {}
25
- @authentication[:plugin] ||= Plugins::NoAuth
26
- @authentication[:login] ||= Views::Pages::SimpleAuthLogin
27
- @authentication[:plugin] = Object.const_get(authentication[:plugin]) if authentication[:plugin].is_a?(String)
28
-
29
- @page_not_found ||= Views::Pages::PageNotFound
30
- @page_not_found = Object.const_get(@page_not_found) if @page_not_found.is_a?(String)
31
- @record_not_found ||= Views::Pages::RecordNotFound
32
- @record_not_found = Object.const_get(@record_not_found) if @record_not_found.is_a?(String)
33
-
34
- @pages ||= {}
35
- @repository ||= Plugins::ActiveRecordRepository
36
- @resources ||= {}
37
- @root_path ||= '/admin'
38
- @root_path = '/' if @root_path == ''
39
- @sections ||= []
47
+ # default values
48
+ DEFAULTS.each do |(option, param), default|
49
+ if param
50
+ self[option] ||= {}
51
+ self[option][param] ||= default
52
+ else
53
+ self[option] ||= default
54
+ end
55
+ end
40
56
 
41
- @root ||= {}
42
- @root[:title] ||= 'TinyAdmin'
43
- @root[:page] ||= Views::Pages::Root
57
+ context.pages ||= {}
58
+ context.resources ||= {}
59
+ @sections ||= []
60
+ @root_path = '/' if @root_path == ''
44
61
 
45
- if @authentication[:plugin] == Plugins::SimpleAuth
46
- @authentication[:logout] ||= ['logout', "#{root_path}/auth/logout"]
62
+ if @authentication[:plugin] <= Plugins::SimpleAuth
63
+ @authentication[:logout] ||= { name: 'logout', path: "#{root_path}/auth/logout" }
47
64
  end
65
+ context.navbar = prepare_navbar(sections, logout: authentication[:logout])
66
+ end
48
67
 
49
- @components ||= {}
50
- @components[:flash] ||= Views::Components::Flash
51
- @components[:head] ||= Views::Components::Head
52
- @components[:navbar] ||= Views::Components::Navbar
53
- @components[:pagination] ||= Views::Components::Pagination
68
+ private
54
69
 
55
- @navbar = prepare_navbar(sections, logout: authentication[:logout])
70
+ def convert_value(key, value)
71
+ if value.is_a?(Hash)
72
+ value.each_key do |key2|
73
+ path = [key, key2]
74
+ if DEFAULTS[path].is_a?(Class) || DEFAULTS[path].is_a?(Module)
75
+ self[key][key2] = Object.const_get(self[key][key2])
76
+ end
77
+ end
78
+ elsif value.is_a?(String) && (DEFAULTS[[key]].is_a?(Class) || DEFAULTS[[key]].is_a?(Module))
79
+ self[key] = Object.const_get(self[key])
80
+ end
56
81
  end
57
82
 
58
83
  def prepare_navbar(sections, logout:)
59
84
  items = sections.each_with_object({}) do |section, list|
60
- slug = section[:slug]
85
+ unless section.is_a?(Hash)
86
+ section_class = Object.const_get(section)
87
+ next unless section_class.respond_to?(:to_h)
88
+
89
+ section = section_class.to_h
90
+ end
91
+
92
+ slug = section[:slug].to_s
61
93
  case section[:type]&.to_sym
62
94
  when :url
63
- list[slug] = [section[:name], section[:url], section[:options]]
95
+ list[slug] = add_url_section(slug, section)
64
96
  when :page
65
- page = section[:page]
66
- pages[slug] = page.is_a?(String) ? Object.const_get(page) : page
67
- list[slug] = [section[:name], route_for(slug)]
97
+ list[slug] = add_page_section(slug, section)
68
98
  when :resource
69
- repository = section[:repository] || settings.repository
70
- resources[slug] = {
71
- model: section[:model].is_a?(String) ? Object.const_get(section[:model]) : section[:model],
72
- repository: repository.is_a?(String) ? Object.const_get(repository) : repository
73
- }
74
- resources[slug].merge! section.slice(:resource, :only, :index, :show, :collection_actions, :member_actions)
75
- hidden = section[:options] && (section[:options].include?(:hidden) || section[:options].include?('hidden'))
76
- list[slug] = [section[:name], route_for(slug)] unless hidden
99
+ list[slug] = add_resource_section(slug, section)
77
100
  end
78
101
  end
79
102
  items['auth/logout'] = logout if logout
80
103
  items
81
104
  end
105
+
106
+ def add_url_section(_slug, section)
107
+ section.slice(:name, :options).tap { _1[:path] = section[:url] }
108
+ end
109
+
110
+ def add_page_section(slug, section)
111
+ page = section[:page]
112
+ context.pages[slug] = page.is_a?(String) ? Object.const_get(page) : page
113
+ { name: section[:name], path: route_for(slug), class: context.pages[slug] }
114
+ end
115
+
116
+ def add_resource_section(slug, section)
117
+ repository = section[:repository] || settings.repository
118
+ context.resources[slug] = {
119
+ model: section[:model].is_a?(String) ? Object.const_get(section[:model]) : section[:model],
120
+ repository: repository.is_a?(String) ? Object.const_get(repository) : repository
121
+ }
122
+ resource_options = section.slice(:resource, :only, :index, :show, :collection_actions, :member_actions)
123
+ resource_options[:only] ||= %i[index show]
124
+ context.resources[slug].merge!(resource_options)
125
+ hidden = section[:options] && (section[:options].include?(:hidden) || section[:options].include?('hidden'))
126
+ { name: section[:name], path: route_for(slug) } unless hidden
127
+ end
82
128
  end
83
129
  end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TinyAdmin
4
+ class Support
5
+ class << self
6
+ def call(value, options: [])
7
+ options.inject(value) { |result, message| result&.send(message) } if value && options&.any?
8
+ end
9
+
10
+ def downcase(value, options: [])
11
+ value&.downcase
12
+ end
13
+
14
+ def format(value, options: [])
15
+ Kernel.format(options.first, value) if value && options&.any?
16
+ end
17
+
18
+ def round(value, options: [])
19
+ value&.round(options&.first&.to_i || 2)
20
+ end
21
+
22
+ def strftime(value, options: [])
23
+ value&.strftime(options&.first || '%Y-%m-%d %H:%M')
24
+ end
25
+
26
+ def to_date(value, options: [])
27
+ value.to_date.to_s if value
28
+ end
29
+
30
+ def upcase(value, options: [])
31
+ value&.upcase
32
+ end
33
+ end
34
+ end
35
+ end
@@ -5,7 +5,8 @@ module TinyAdmin
5
5
  def params_to_s(params)
6
6
  list = params.each_with_object([]) do |(param, value), result|
7
7
  if value.is_a?(Hash)
8
- result.concat(value.map { |k, v| "#{param}[#{k}]=#{v}" })
8
+ values = value.map { |key, val| "#{param}[#{key}]=#{val}" }
9
+ result.concat(values)
9
10
  else
10
11
  result.push(["#{param}=#{value}"])
11
12
  end
@@ -18,23 +19,30 @@ module TinyAdmin
18
19
  page.options = options
19
20
  page.head_component = settings.components[:head]&.new
20
21
  page.flash_component = settings.components[:flash]&.new
21
- page.navbar_component = settings.components[:navbar]&.new(
22
+ page.navbar_component = settings.components[:navbar]&.new
23
+ page.navbar_component&.update_attributes(
22
24
  current_slug: context&.slug,
23
25
  root_path: settings.root_path,
24
26
  root_title: settings.root[:title],
25
- items: options&.include?(:no_menu) ? [] : settings.navbar
27
+ items: options&.include?(:no_menu) ? [] : context&.navbar
26
28
  )
27
-
28
29
  yield(page) if block_given?
29
30
  end
30
31
  end
31
32
 
32
- def route_for(section, reference: nil, action: nil)
33
+ def route_for(section, reference: nil, action: nil, query: nil)
33
34
  root_path = settings.root_path == '/' ? nil : settings.root_path
34
35
  route = [root_path, section, reference, action].compact.join("/")
36
+ route << "?#{query}" if query
35
37
  route[0] == '/' ? route : route.prepend('/')
36
38
  end
37
39
 
40
+ def to_label(string)
41
+ return '' unless string
42
+
43
+ string.respond_to?(:humanize) ? string.humanize : string.tr('_', ' ').capitalize
44
+ end
45
+
38
46
  def context
39
47
  TinyAdmin::Context.instance
40
48
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module TinyAdmin
4
- VERSION = '0.3.0'
4
+ VERSION = '0.5.0'
5
5
  end
@@ -4,12 +4,9 @@ module TinyAdmin
4
4
  module Views
5
5
  module Actions
6
6
  class Index < DefaultLayout
7
- attr_accessor :actions, :fields, :filters, :pagination_component, :prepare_record, :records
7
+ attr_accessor :actions, :fields, :filters, :links, :pagination_component, :prepare_record, :records
8
8
 
9
9
  def template
10
- @fields = fields.each_with_object({}) { |field, result| result[field.name] = field }
11
- @filters ||= {}
12
-
13
10
  super do
14
11
  div(class: 'index') {
15
12
  div(class: 'row') {
@@ -17,19 +14,12 @@ module TinyAdmin
17
14
  h1(class: 'title') { title }
18
15
  }
19
16
  div(class: 'col-8') {
20
- ul(class: 'nav justify-content-end') {
21
- (actions || []).each do |action|
22
- li(class: 'nav-item') {
23
- href = route_for(context.slug, action: action)
24
- a(href: href, class: 'nav-link btn btn-outline-secondary') { action }
25
- }
26
- end
27
- }
17
+ actions_buttons
28
18
  }
29
19
  }
30
20
 
31
21
  div(class: 'row') {
32
- div_class = filters.any? ? 'col-9' : 'col-12'
22
+ div_class = filters&.any? ? 'col-9' : 'col-12'
33
23
  div(class: div_class) {
34
24
  table(class: 'table') {
35
25
  table_header if fields.any?
@@ -38,10 +28,11 @@ module TinyAdmin
38
28
  }
39
29
  }
40
30
 
41
- if filters.any?
31
+ if filters&.any?
42
32
  div(class: 'col-3') {
43
- filters_form_attrs = { section_path: route_for(context.slug), filters: filters }
44
- render TinyAdmin::Views::Components::FiltersForm.new(**filters_form_attrs)
33
+ filters_form = TinyAdmin::Views::Components::FiltersForm.new
34
+ filters_form.update_attributes(section_path: route_for(context.slug), filters: filters)
35
+ render filters_form
45
36
  }
46
37
  end
47
38
  }
@@ -57,7 +48,9 @@ module TinyAdmin
57
48
  thead {
58
49
  tr {
59
50
  fields.each_value do |field|
60
- td(class: "field-header-#{field.name} field-header-type-#{field.type}") { field.title }
51
+ td(class: "field-header-#{field.name} field-header-type-#{field.type}") {
52
+ field.options[:header] || field.title
53
+ }
61
54
  end
62
55
  td { whitespace }
63
56
  }
@@ -73,15 +66,46 @@ module TinyAdmin
73
66
  field = fields[key]
74
67
  td(class: "field-value-#{field.name} field-value-type-#{field.type}") {
75
68
  if field.options && field.options[:link_to]
76
- reference = record.send(field.options[:field])
77
- a(href: route_for(field.options[:link_to], reference: reference)) { value }
69
+ a(href: route_for(field.options[:link_to], reference: value)) {
70
+ field.apply_call_option(record) || value
71
+ }
78
72
  else
79
73
  value
80
74
  end
81
75
  }
82
76
  end
83
- td(class: 'actions') {
84
- a(href: route_for(context.slug, reference: record.id)) { 'show' }
77
+
78
+ td(class: 'actions p-1') {
79
+ div(class: 'btn-group btn-group-sm') {
80
+ link_class = 'btn btn-outline-secondary'
81
+ if links
82
+ links.each do |link|
83
+ whitespace
84
+ if link == 'show'
85
+ a(href: route_for(context.slug, reference: record.id), class: link_class) { 'show' }
86
+ else
87
+ a(href: route_for(context.slug, reference: record.id, action: link), class: link_class) {
88
+ to_label(link)
89
+ }
90
+ end
91
+ end
92
+ else
93
+ a(href: route_for(context.slug, reference: record.id), class: link_class) { 'show' }
94
+ end
95
+ }
96
+ }
97
+ }
98
+ end
99
+ }
100
+ end
101
+
102
+ def actions_buttons
103
+ ul(class: 'nav justify-content-end') {
104
+ (actions || {}).each do |action, action_class|
105
+ li(class: 'nav-item mx-1') {
106
+ href = route_for(context.slug, action: action)
107
+ a(href: href, class: 'nav-link btn btn-outline-secondary') {
108
+ action_class.respond_to?(:title) ? action_class.title : action
85
109
  }
86
110
  }
87
111
  end
@@ -4,14 +4,7 @@ module TinyAdmin
4
4
  module Views
5
5
  module Actions
6
6
  class Show < DefaultLayout
7
- attr_reader :fields, :prepare_record, :record
8
- attr_accessor :actions
9
-
10
- def setup_record(record:, fields:, prepare_record:)
11
- @record = record
12
- @fields = fields
13
- @prepare_record = prepare_record
14
- end
7
+ attr_accessor :actions, :fields, :prepare_record, :record
15
8
 
16
9
  def template
17
10
  super do
@@ -21,27 +14,21 @@ module TinyAdmin
21
14
  h1(class: 'title') { title }
22
15
  }
23
16
  div(class: 'col-8') {
24
- ul(class: 'nav justify-content-end') {
25
- (actions || []).each do |action|
26
- li(class: 'nav-item') {
27
- href = route_for(context.slug, reference: context.reference, action: action)
28
- a(href: href, class: 'nav-link btn btn-outline-secondary') { action }
29
- }
30
- end
31
- }
17
+ actions_buttons
32
18
  }
33
19
  }
34
20
 
35
- prepare_record.call(record).each_with_index do |(_key, value), index|
36
- field = fields[index]
21
+ prepare_record.call(record).each do |key, value|
22
+ field = fields[key]
37
23
  div(class: "field-#{field.name} row lh-lg") {
38
24
  if field
39
- div(class: 'field-header col-2') { field.title }
25
+ div(class: 'field-header col-2') { field.options[:header] || field.title }
40
26
  end
41
27
  div(class: 'field-value col-10') {
42
- if field.options && field.options[:link_to]
43
- reference = record.send(field.options[:field])
44
- a(href: route_for(field.options[:link_to], reference: reference)) { value }
28
+ if field.options[:link_to]
29
+ a(href: route_for(field.options[:link_to], reference: value)) {
30
+ field.apply_call_option(record) || value
31
+ }
45
32
  else
46
33
  value
47
34
  end
@@ -51,6 +38,21 @@ module TinyAdmin
51
38
  }
52
39
  end
53
40
  end
41
+
42
+ private
43
+
44
+ def actions_buttons
45
+ ul(class: 'nav justify-content-end') {
46
+ (actions || {}).each do |action, action_class|
47
+ li(class: 'nav-item mx-1') {
48
+ href = route_for(context.slug, reference: context.reference, action: action)
49
+ a(href: href, class: 'nav-link btn btn-outline-secondary') {
50
+ action_class.respond_to?(:title) ? action_class.title : action
51
+ }
52
+ }
53
+ end
54
+ }
55
+ end
54
56
  end
55
57
  end
56
58
  end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TinyAdmin
4
+ module Views
5
+ module Components
6
+ class BasicComponent < Phlex::HTML
7
+ def update_attributes(attributes)
8
+ attributes.each do |key, value|
9
+ send("#{key}=", value)
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -3,13 +3,8 @@
3
3
  module TinyAdmin
4
4
  module Views
5
5
  module Components
6
- class FiltersForm < Phlex::HTML
7
- attr_reader :filters, :section_path
8
-
9
- def initialize(section_path:, filters:)
10
- @section_path = section_path
11
- @filters = filters
12
- end
6
+ class FiltersForm < BasicComponent
7
+ attr_accessor :filters, :section_path
13
8
 
14
9
  def template
15
10
  form(class: 'form_filters', method: 'get') {
@@ -3,24 +3,21 @@
3
3
  module TinyAdmin
4
4
  module Views
5
5
  module Components
6
- class Flash < Phlex::HTML
7
- attr_reader :errors, :notices, :warnings
6
+ class Flash < BasicComponent
7
+ attr_accessor :messages
8
8
 
9
9
  def template
10
+ @messages ||= {}
11
+ notices = messages[:notices]
12
+ warnings = messages[:warnings]
13
+ errors = messages[:errors]
14
+
10
15
  div(class: 'flash') {
11
16
  div(class: 'notices alert alert-success', role: 'alert') { notices.join(', ') } if notices&.any?
12
17
  div(class: 'notices alert alert-warning', role: 'alert') { warnings.join(', ') } if warnings&.any?
13
18
  div(class: 'notices alert alert-danger', role: 'alert') { errors.join(', ') } if errors&.any?
14
19
  }
15
20
  end
16
-
17
- def update(messages:)
18
- return unless messages
19
-
20
- @notices = messages[:notices]
21
- @warnings = messages[:warnings]
22
- @errors = messages[:errors]
23
- end
24
21
  end
25
22
  end
26
23
  end
@@ -3,8 +3,8 @@
3
3
  module TinyAdmin
4
4
  module Views
5
5
  module Components
6
- class Head < Phlex::HTML
7
- attr_reader :extra_styles, :page_title, :style_links
6
+ class Head < BasicComponent
7
+ attr_accessor :extra_styles, :page_title, :style_links
8
8
 
9
9
  def template
10
10
  head {
@@ -19,12 +19,6 @@ module TinyAdmin
19
19
  style { extra_styles } if extra_styles
20
20
  }
21
21
  end
22
-
23
- def update(page_title, style_links: [], extra_styles: nil)
24
- @page_title = page_title
25
- @style_links = style_links
26
- @extra_styles = extra_styles
27
- end
28
22
  end
29
23
  end
30
24
  end