tiny_admin 0.3.0 → 0.5.0

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.
@@ -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