tiny_admin 0.6.0 → 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0dfe9b2fef29073201e228daf3a7d89003902f8d98108e58cb8245fdb9a533e3
4
- data.tar.gz: 6634891a9bbc2585549a759cd061a25234b834ebdcc0b77fbbb9bd2297b0a626
3
+ metadata.gz: 71d42ca63e3a410de95780d584cdfb61dd94feb95b503f5f55379efabec9477e
4
+ data.tar.gz: 96c4b375cb65327635199296d11f54335e6c5266dacce39a7316e0eb2844046c
5
5
  SHA512:
6
- metadata.gz: 78b49e3f8b7efe941e24734692db4e4baaa2159f58a10f49ce0563f8c4cfa5950f3e155184016faff12c415ff1a6632d4ee06cd61659a466d170d4b7954becf7
7
- data.tar.gz: 16c4fe68dca49b84c4c8ca834c9e3c2ce2304f5a78dc378cfdde837462a96a4e00030e5098083d025ad9ab28060ea2aa581725434ffeec1582715e5fa725ff28
6
+ metadata.gz: d377537524247253a871e016d3d5e688e47ab07d40c6d33814c5dfc0be761e734d3de3c8c23d3e3981d3f949ce15d230312b4c71fb2d7d6f33aeb45cc352223f
7
+ data.tar.gz: 4ddb16c2f1fbfefced95deb426cf369d28201866f800eee1f4b14895bd938233f6f67bf51d784a7edf340b48d7a75230a86b679db9ec71c07f18b726c23971a4
data/README.md CHANGED
@@ -1,6 +1,9 @@
1
1
  # Tiny Admin
2
2
 
3
- [![Gem Version](https://badge.fury.io/rb/tiny_admin.svg)](https://badge.fury.io/rb/tiny_admin) [![Linters](https://github.com/blocknotes/tiny_admin/actions/workflows/linters.yml/badge.svg)](https://github.com/blocknotes/tiny_admin/actions/workflows/linters.yml) [![Specs](https://github.com/blocknotes/tiny_admin/actions/workflows/specs.yml/badge.svg)](https://github.com/blocknotes/tiny_admin/actions/workflows/specs.yml)
3
+ [![Gem Version](https://badge.fury.io/rb/tiny_admin.svg)](https://badge.fury.io/rb/tiny_admin)
4
+ [![Gem Downloads](https://badgen.net/rubygems/dt/tiny_admin)](https://rubygems.org/gems/tiny_admin)
5
+ [![Linters](https://github.com/blocknotes/tiny_admin/actions/workflows/linters.yml/badge.svg)](https://github.com/blocknotes/tiny_admin/actions/workflows/linters.yml)
6
+ [![Specs](https://github.com/blocknotes/tiny_admin/actions/workflows/specs.yml/badge.svg)](https://github.com/blocknotes/tiny_admin/actions/workflows/specs.yml)
4
7
 
5
8
  A compact and composable dashboard component for Ruby.
6
9
 
@@ -16,7 +19,7 @@ Please ⭐ if you like it.
16
19
 
17
20
  ## Install
18
21
 
19
- - Add to your Gemfile: `gem 'tiny_admin', '~> 0.6'`
22
+ - Add to your Gemfile: `gem 'tiny_admin', '~> 0.8'`
20
23
  - Mount the app in a route (check some examples with: Hanami, Rails, Roda and standalone in [extra](extra))
21
24
  + in Rails, update _config/routes.rb_: `mount TinyAdmin::Router => '/admin'`
22
25
  - Configure the dashboard using `TinyAdmin.configure` and/or `TinyAdmin.configure_from_file` with a YAML config file (see [configuration](#configuration) below):
@@ -80,6 +83,9 @@ The following options are supported:
80
83
  - `title` (String): root section's title;
81
84
  - `page` (String): a view object to render;
82
85
  - `redirect` (String): alternative to _page_ option - redirects to a specific slug;
86
+ - `widgets` (Array): list of widgets (as View components) to present.
87
+
88
+ > 📚 [Wiki Root page](https://github.com/blocknotes/tiny_admin/wiki/Root) available
83
89
 
84
90
  Example:
85
91
 
@@ -87,10 +93,15 @@ Example:
87
93
  root:
88
94
  title: MyAdmin
89
95
  redirect: posts
96
+ widgets:
97
+ - LatestAuthorsWidget
98
+ - LatestPostsWidget
90
99
  ```
91
100
 
92
101
  `helper_class` (String): class or module with helper methods, used for attributes' formatters.
93
102
 
103
+ > 📚 [Wiki Helper methods page](https://github.com/blocknotes/tiny_admin/wiki/Helper-methods) available
104
+
94
105
  `page_not_found` (String): a view object to render when a missing page is requested.
95
106
 
96
107
  `record_not_found` (String): a view object to render when a missing record is requested.
@@ -106,11 +117,15 @@ root:
106
117
 
107
118
  `extra_styles` (String): inline CSS styles.
108
119
 
120
+ > 📚 [Wiki Styles and scripts page](https://github.com/blocknotes/tiny_admin/wiki/Styles-and-scripts) available
121
+
109
122
  `authentication` (Hash): define the authentication method, properties:
110
123
 
111
124
  - `plugin` (String): a plugin class to use (ex. `TinyAdmin::Plugins::SimpleAuth`);
112
125
  - `password` (String): a password hash used by _SimpleAuth_ plugin (generated with `Digest::SHA512.hexdigest("some password")`).
113
126
 
127
+ > 📚 [Wiki Authentication page](https://github.com/blocknotes/tiny_admin/wiki/Authentication) available
128
+
114
129
  Example:
115
130
 
116
131
  ```yml
@@ -124,8 +139,11 @@ authentication:
124
139
  - `slug` (String): section reference identifier;
125
140
  - `name` (String): section's title;
126
141
  - `type` (String): the type of section: `content`, `page`, `resource` or `url`;
142
+ - `widgets` (Array): list of widgets (as View components) to present;
127
143
  - other properties depends on the section's type.
128
144
 
145
+ > 📚 [Wiki Pages page](https://github.com/blocknotes/tiny_admin/wiki/Pages) available
146
+
129
147
  For _content_ sections:
130
148
 
131
149
  - `content` (String): the HTML content to present.
@@ -139,6 +157,9 @@ type: content
139
157
  content: >
140
158
  <h1>Test content!</h1>
141
159
  <p>Some test content</p>
160
+ widgets:
161
+ - LatestAuthorsWidget
162
+ - LatestPostsWidget
142
163
  ```
143
164
 
144
165
  For _url_ sections:
@@ -175,10 +196,14 @@ For _resource_ sections:
175
196
 
176
197
  - `model` (String): the class to use to fetch the data on an item of a collection;
177
198
  - `repository` (String): the class to get the properties related to the model;
199
+
200
+ > 📚 [Wiki Repository page](https://github.com/blocknotes/tiny_admin/wiki/Repository) available
201
+
178
202
  - `index` (Hash): collection's action options (see below);
179
203
  - `show` (Hash): detail's action options (see below);
180
204
  - `collection_actions` (Array of hashes): custom collection's actions;
181
205
  - `member_actions` (Array of hashes): custom details's actions;
206
+ - `widgets` (Array): list of widgets (as View components) to present;
182
207
  - `only` (Array of strings): list of supported actions (ex. `index`);
183
208
  - `options` (Array of strings): resource options (ex. `hidden`).
184
209
 
@@ -256,6 +281,9 @@ Example:
256
281
  header: The author
257
282
  link_to: authors
258
283
  call: author, name
284
+ widgets:
285
+ - LatestAuthorsWidget
286
+ - LatestPostsWidget
259
287
  ```
260
288
 
261
289
  ### Sample
@@ -280,6 +308,9 @@ authentication:
280
308
  # password: 'f1891cea80fc05e433c943254c6bdabc159577a02a7395dfebbfbc4f7661d4af56f2d372131a45936de40160007368a56ef216a30cb202c66d3145fd24380906'
281
309
  root:
282
310
  title: Test Admin
311
+ widgets:
312
+ - LatestAuthorsWidget
313
+ - LatestPostsWidget
283
314
  # page: RootPage
284
315
  helper_class: AdminHelper
285
316
  page_not_found: PageNotFound
@@ -3,34 +3,38 @@
3
3
  module TinyAdmin
4
4
  module Actions
5
5
  class Index < BasicAction
6
- attr_reader :current_page,
6
+ attr_reader :context,
7
+ :current_page,
7
8
  :fields_options,
8
- :filters_list,
9
9
  :links,
10
+ :options,
10
11
  :pagination,
11
12
  :pages,
12
13
  :params,
13
14
  :query_string,
14
- :repository,
15
- :sort
15
+ :repository
16
16
 
17
17
  def call(app:, context:, options:)
18
+ @context = context
19
+ @options = options || {}
18
20
  evaluate_options(options)
19
21
  fields = repository.fields(options: fields_options)
20
- filters = prepare_filters(fields, filters_list)
21
- records, total_count = repository.list(page: current_page, limit: pagination, filters: filters, sort: sort)
22
+ filters = prepare_filters(fields)
23
+ records, count = repository.list(page: current_page, limit: pagination, filters: filters, sort: options[:sort])
24
+ attributes = {
25
+ actions: context.actions,
26
+ fields: fields,
27
+ filters: filters,
28
+ links: options[:links],
29
+ prepare_record: ->(record) { repository.index_record_attrs(record, fields: fields_options) },
30
+ records: records,
31
+ slug: context.slug,
32
+ title: repository.index_title,
33
+ widgets: options[:widgets]
34
+ }
22
35
 
23
- prepare_page(Views::Actions::Index) do |page|
24
- setup_pagination(page, TinyAdmin.settings.components[:pagination], total_count: total_count)
25
- page.update_attributes(
26
- actions: context.actions,
27
- fields: fields,
28
- filters: filters,
29
- links: links,
30
- prepare_record: ->(record) { repository.index_record_attrs(record, fields: fields_options) },
31
- records: records,
32
- title: repository.index_title
33
- )
36
+ prepare_page(Views::Actions::Index, slug: context.slug, attributes: attributes) do |page|
37
+ setup_pagination(page, TinyAdmin.settings.components[:pagination], total_count: count)
34
38
  end
35
39
  end
36
40
 
@@ -40,17 +44,13 @@ module TinyAdmin
40
44
  @fields_options = attribute_options(options[:attributes])
41
45
  @params = context.request.params
42
46
  @repository = context.repository
43
- @filters_list = options[:filters]
44
47
  @pagination = options[:pagination] || 10
45
- @sort = options[:sort]
46
- @links = options[:links]
47
-
48
48
  @current_page = (params['p'] || 1).to_i
49
49
  @query_string = params_to_s(params.except('p'))
50
50
  end
51
51
 
52
- def prepare_filters(fields, filters_list)
53
- filters = (filters_list || []).map { _1.is_a?(Hash) ? _1 : { field: _1 } }
52
+ def prepare_filters(fields)
53
+ filters = (options[:filters] || []).map { _1.is_a?(Hash) ? _1 : { field: _1 } }
54
54
  filters = filters.each_with_object({}) { |filter, result| result[filter[:field]] = filter }
55
55
  values = (params['q'] || {})
56
56
  fields.each_with_object({}) do |(name, field), result|
@@ -8,16 +8,18 @@ module TinyAdmin
8
8
  repository = context.repository
9
9
  record = repository.find(context.reference)
10
10
  prepare_record = ->(record_data) { repository.show_record_attrs(record_data, fields: fields_options) }
11
+ attributes = {
12
+ actions: context.actions,
13
+ fields: repository.fields(options: fields_options),
14
+ prepare_record: prepare_record,
15
+ record: record,
16
+ reference: context.reference,
17
+ slug: context.slug,
18
+ title: repository.show_title(record),
19
+ widgets: options[:widgets]
20
+ }
11
21
 
12
- prepare_page(Views::Actions::Show) do |page|
13
- page.update_attributes(
14
- actions: context.actions,
15
- fields: repository.fields(options: fields_options),
16
- prepare_record: prepare_record,
17
- record: record,
18
- title: repository.show_title(record)
19
- )
20
- end
22
+ prepare_page(Views::Actions::Show, slug: context.slug, attributes: attributes)
21
23
  rescue Plugins::BaseRepository::RecordNotFound => _e
22
24
  prepare_page(options[:record_not_found_page] || Views::Pages::RecordNotFound)
23
25
  end
@@ -12,7 +12,11 @@ module TinyAdmin
12
12
  end
13
13
 
14
14
  r.post 'unauthenticated' do
15
- render_login(warnings: ['Failed to authenticate'])
15
+ warning = TinyAdmin.settings.helper_class.label_for(
16
+ 'Failed to authenticate',
17
+ options: ['authentication.unauthenticated']
18
+ )
19
+ render_login(warnings: [warning])
16
20
  end
17
21
 
18
22
  r.get 'logout' do
@@ -24,7 +28,10 @@ module TinyAdmin
24
28
  private
25
29
 
26
30
  def render_login(notices: nil, warnings: nil, errors: nil)
27
- page = prepare_page(TinyAdmin.settings.authentication[:login], options: %i[no_menu compact_layout])
31
+ login = TinyAdmin.settings.authentication[:login]
32
+ return unless login
33
+
34
+ page = prepare_page(login, options: %i[no_menu compact_layout])
28
35
  page.messages = {
29
36
  notices: notices || flash['notices'],
30
37
  warnings: warnings || flash['warnings'],
@@ -1,18 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module TinyAdmin
4
- class Context
5
- include Singleton
6
-
7
- attr_accessor :actions,
8
- :navbar,
9
- :pages,
10
- :reference,
11
- :repository,
12
- :request,
13
- :resources,
14
- :router,
15
- :settings,
16
- :slug
17
- end
4
+ Context = Struct.new(
5
+ :actions,
6
+ :reference,
7
+ :repository,
8
+ :request,
9
+ :router,
10
+ :slug,
11
+ keyword_init: true
12
+ )
18
13
  end
@@ -16,6 +16,19 @@ module TinyAdmin
16
16
  messages.inject(target) { |result, msg| result&.send(msg) } if messages.any?
17
17
  end
18
18
 
19
+ def translate_value(value)
20
+ if options && options[:method]
21
+ method, *args = options[:method].split(',').map(&:strip)
22
+ if options[:converter]
23
+ Object.const_get(options[:converter]).send(method, value, options: args || [])
24
+ else
25
+ TinyAdmin.settings.helper_class.send(method, value, options: args || [])
26
+ end
27
+ else
28
+ value&.to_s
29
+ end
30
+ end
31
+
19
32
  class << self
20
33
  def create_field(name:, title: nil, type: nil, options: {})
21
34
  field_name = name.to_s
@@ -6,10 +6,7 @@ module TinyAdmin
6
6
  def index_record_attrs(record, fields: nil)
7
7
  return record.attributes.transform_values(&:to_s) unless fields
8
8
 
9
- fields.to_h do |name, field|
10
- value = record.send(name)
11
- [name, translate_value(value, field)]
12
- end
9
+ fields.to_h { [_1, record.send(_1)] }
13
10
  end
14
11
 
15
12
  def index_title
@@ -10,20 +10,6 @@ module TinyAdmin
10
10
  def initialize(model)
11
11
  @model = model
12
12
  end
13
-
14
- def translate_value(value, field)
15
- if field[:method]
16
- method, *options = field[:method].split(',').map(&:strip)
17
- if field[:converter]
18
- converter = Object.const_get(field[:converter])
19
- converter.send(method, value, options: options || [])
20
- else
21
- TinyAdmin.settings.helper_class.send(method, value, options: options || [])
22
- end
23
- else
24
- value&.to_s
25
- end
26
- end
27
13
  end
28
14
  end
29
15
  end
@@ -2,14 +2,14 @@
2
2
 
3
3
  module TinyAdmin
4
4
  class Router < BasicApp
5
- TinyAdmin.settings.load_settings
5
+ extend Forwardable
6
+
7
+ def_delegator TinyAdmin, :route_for
6
8
 
7
9
  route do |r|
8
- context.router = r
9
- context.settings = TinyAdmin.settings
10
+ TinyAdmin.settings.load_settings
10
11
 
11
12
  r.on 'auth' do
12
- context.slug = nil
13
13
  r.run Authentication
14
14
  end
15
15
 
@@ -26,15 +26,14 @@ module TinyAdmin
26
26
  end
27
27
 
28
28
  r.post '' do
29
- context.slug = nil
30
29
  r.redirect TinyAdmin.settings.root_path
31
30
  end
32
31
 
33
- context.pages.each do |slug, page_data|
32
+ store.pages.each do |slug, page_data|
34
33
  setup_page_route(r, slug, page_data)
35
34
  end
36
35
 
37
- context.resources.each do |slug, options|
36
+ store.resources.each do |slug, options|
38
37
  setup_resource_routes(r, slug, options: options || {})
39
38
  end
40
39
 
@@ -43,6 +42,10 @@ module TinyAdmin
43
42
 
44
43
  private
45
44
 
45
+ def store
46
+ @store ||= TinyAdmin.settings.store
47
+ end
48
+
46
49
  def render_page(page)
47
50
  if page.respond_to?(:messages=)
48
51
  page.messages = { notices: flash['notices'], warnings: flash['warnings'], errors: flash['errors'] }
@@ -51,76 +54,83 @@ module TinyAdmin
51
54
  end
52
55
 
53
56
  def root_route(router)
54
- context.slug = nil
55
57
  if TinyAdmin.settings.root[:redirect]
56
58
  router.redirect route_for(TinyAdmin.settings.root[:redirect])
57
59
  else
58
- page = TinyAdmin.settings.root[:page]
59
- page_class = page.is_a?(String) ? Object.const_get(page) : page
60
- render_page prepare_page(page_class)
60
+ page_class = to_class(TinyAdmin.settings.root[:page])
61
+ render_page prepare_page(page_class, attributes: TinyAdmin.settings.root.slice(:content, :title, :widgets))
61
62
  end
62
63
  end
63
64
 
64
65
  def setup_page_route(router, slug, page_data)
65
66
  router.get slug do
66
- context.slug = slug
67
- page = prepare_page(page_data[:class])
68
- page.update_attributes(content: page_data[:content]) if page_data[:content]
69
- render_page page
67
+ attributes = page_data.slice(:content, :title, :widgets)
68
+ render_page prepare_page(page_data[:class], slug: slug, attributes: attributes)
70
69
  end
71
70
  end
72
71
 
73
72
  def setup_resource_routes(router, slug, options:)
74
73
  router.on slug do
75
- context.slug = slug
76
- setup_collection_routes(router, options: options)
77
- setup_member_routes(router, options: options)
74
+ setup_collection_routes(router, slug, options: options)
75
+ setup_member_routes(router, slug, options: options)
78
76
  end
79
77
  end
80
78
 
81
- def setup_collection_routes(router, options:)
82
- context.repository = options[:repository].new(options[:model])
79
+ def setup_collection_routes(router, slug, options:)
80
+ repository = options[:repository].new(options[:model])
83
81
  action_options = options[:index] || {}
84
82
 
85
83
  # Custom actions
86
84
  custom_actions = setup_custom_actions(
87
85
  router,
88
86
  options[:collection_actions],
89
- repository: context.repository,
90
- options: action_options
87
+ options: action_options,
88
+ repository: repository,
89
+ slug: slug
91
90
  )
92
91
 
93
92
  # Index
94
93
  if options[:only].include?(:index) || options[:only].include?('index')
95
94
  router.is do
96
- context.actions = custom_actions
97
- context.request = request
95
+ context = Context.new(
96
+ actions: custom_actions,
97
+ repository: repository,
98
+ request: request,
99
+ router: router,
100
+ slug: slug
101
+ )
98
102
  index_action = TinyAdmin::Actions::Index.new
99
103
  render_page index_action.call(app: self, context: context, options: action_options)
100
104
  end
101
105
  end
102
106
  end
103
107
 
104
- def setup_member_routes(router, options:)
105
- context.repository = options[:repository].new(options[:model])
108
+ def setup_member_routes(router, slug, options:)
109
+ repository = options[:repository].new(options[:model])
106
110
  action_options = (options[:show] || {}).merge(record_not_found_page: TinyAdmin.settings.record_not_found)
107
111
 
108
112
  router.on String do |reference|
109
- context.reference = reference
110
-
111
113
  # Custom actions
112
114
  custom_actions = setup_custom_actions(
113
115
  router,
114
116
  options[:member_actions],
115
- repository: context.repository,
116
- options: action_options
117
+ options: action_options,
118
+ repository: repository,
119
+ slug: slug,
120
+ reference: reference
117
121
  )
118
122
 
119
123
  # Show
120
124
  if options[:only].include?(:show) || options[:only].include?('show')
121
125
  router.is do
122
- context.actions = custom_actions
123
- context.request = request
126
+ context = Context.new(
127
+ actions: custom_actions,
128
+ reference: reference,
129
+ repository: repository,
130
+ request: request,
131
+ router: router,
132
+ slug: slug
133
+ )
124
134
  show_action = TinyAdmin::Actions::Show.new
125
135
  render_page show_action.call(app: self, context: context, options: action_options)
126
136
  end
@@ -128,15 +138,20 @@ module TinyAdmin
128
138
  end
129
139
  end
130
140
 
131
- def setup_custom_actions(router, custom_actions, repository:, options:)
132
- context.repository = repository
141
+ def setup_custom_actions(router, custom_actions, options:, repository:, slug:, reference: nil)
133
142
  (custom_actions || []).each_with_object({}) do |custom_action, result|
134
143
  action_slug, action = custom_action.first
135
- action_class = action.is_a?(String) ? Object.const_get(action) : action
144
+ action_class = to_class(action)
136
145
 
137
146
  router.get action_slug.to_s do
138
- context.actions = {}
139
- context.request = request
147
+ context = Context.new(
148
+ actions: {},
149
+ reference: reference,
150
+ repository: repository,
151
+ request: request,
152
+ router: router,
153
+ slug: slug
154
+ )
140
155
  custom_action = action_class.new
141
156
  render_page custom_action.call(app: self, context: context, options: options)
142
157
  end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TinyAdmin
4
+ class Section
5
+ attr_reader :name, :options, :path, :slug
6
+
7
+ def initialize(name:, slug: nil, path: nil, options: {})
8
+ @name = name
9
+ @options = options
10
+ @path = path || TinyAdmin.route_for(slug)
11
+ @slug = slug
12
+ end
13
+ end
14
+ end
@@ -3,11 +3,11 @@
3
3
  module TinyAdmin
4
4
  class Settings
5
5
  include Singleton
6
- include Utils
7
6
 
8
7
  DEFAULTS = {
9
8
  %i[authentication plugin] => Plugins::NoAuth,
10
9
  %i[authentication login] => Views::Pages::SimpleAuthLogin,
10
+ %i[components field_value] => Views::Components::FieldValue,
11
11
  %i[components flash] => Views::Components::Flash,
12
12
  %i[components head] => Views::Components::Head,
13
13
  %i[components navbar] => Views::Components::Navbar,
@@ -19,7 +19,8 @@ module TinyAdmin
19
19
  %i[repository] => Plugins::ActiveRecordRepository,
20
20
  %i[root_path] => '/admin',
21
21
  %i[root page] => Views::Pages::Root,
22
- %i[root title] => 'TinyAdmin'
22
+ %i[root title] => 'TinyAdmin',
23
+ %i[sections] => []
23
24
  }.freeze
24
25
 
25
26
  OPTIONS = %i[
@@ -38,6 +39,8 @@ module TinyAdmin
38
39
  style_links
39
40
  ].freeze
40
41
 
42
+ attr_reader :store
43
+
41
44
  OPTIONS.each do |option|
42
45
  define_method(option) do
43
46
  self[option]
@@ -70,15 +73,14 @@ module TinyAdmin
70
73
  end
71
74
  end
72
75
 
73
- context.pages ||= {}
74
- context.resources ||= {}
75
- self.sections ||= []
76
+ @store ||= TinyAdmin::Store.new(self)
76
77
  self.root_path = '/' if root_path == ''
77
78
 
78
79
  if authentication[:plugin] <= Plugins::SimpleAuth
79
- authentication[:logout] ||= { name: 'logout', path: "#{root_path}/auth/logout" }
80
+ logout_path = "#{root_path}/auth/logout"
81
+ authentication[:logout] ||= TinyAdmin::Section.new(name: 'logout', slug: 'logout', path: logout_path)
80
82
  end
81
- context.navbar = prepare_navbar(sections, logout: authentication[:logout])
83
+ store.prepare_sections(sections, logout: authentication[:logout])
82
84
  end
83
85
 
84
86
  def reset!
@@ -97,7 +99,7 @@ module TinyAdmin
97
99
  if value.is_a?(Hash)
98
100
  value.each_key do |key2|
99
101
  path = [key, key2]
100
- if DEFAULTS[path].is_a?(Class) || DEFAULTS[path].is_a?(Module)
102
+ if (DEFAULTS[path].is_a?(Class) || DEFAULTS[path].is_a?(Module)) && self[key][key2].is_a?(String)
101
103
  self[key][key2] = Object.const_get(self[key][key2])
102
104
  end
103
105
  end
@@ -105,59 +107,5 @@ module TinyAdmin
105
107
  self[key] = Object.const_get(self[key])
106
108
  end
107
109
  end
108
-
109
- def prepare_navbar(sections, logout:)
110
- items = sections.each_with_object({}) do |section, list|
111
- unless section.is_a?(Hash)
112
- section_class = section.is_a?(String) ? Object.const_get(section) : section
113
- next unless section_class.respond_to?(:to_h)
114
-
115
- section = section_class.to_h
116
- end
117
-
118
- slug = section[:slug].to_s
119
- case section[:type]&.to_sym
120
- when :content
121
- list[slug] = add_content_section(slug, section)
122
- when :page
123
- list[slug] = add_page_section(slug, section)
124
- when :resource
125
- list[slug] = add_resource_section(slug, section)
126
- when :url
127
- list[slug] = add_url_section(slug, section)
128
- end
129
- end
130
- items['auth/logout'] = logout if logout
131
- items
132
- end
133
-
134
- def add_content_section(slug, section)
135
- context.pages[slug] = { class: content_page, content: section[:content] }
136
- { name: section[:name], path: route_for(slug), class: content_page }
137
- end
138
-
139
- def add_page_section(slug, section)
140
- page = section[:page]
141
- page_class = page.is_a?(String) ? Object.const_get(page) : page
142
- context.pages[slug] = { class: page_class }
143
- { name: section[:name], path: route_for(slug), class: page_class }
144
- end
145
-
146
- def add_resource_section(slug, section)
147
- repo = section[:repository] || repository
148
- context.resources[slug] = {
149
- model: section[:model].is_a?(String) ? Object.const_get(section[:model]) : section[:model],
150
- repository: repo.is_a?(String) ? Object.const_get(repo) : repo
151
- }
152
- resource_options = section.slice(:resource, :only, :index, :show, :collection_actions, :member_actions)
153
- resource_options[:only] ||= %i[index show]
154
- context.resources[slug].merge!(resource_options)
155
- hidden = section[:options] && (section[:options].include?(:hidden) || section[:options].include?('hidden'))
156
- { name: section[:name], path: route_for(slug) } unless hidden
157
- end
158
-
159
- def add_url_section(_slug, section)
160
- section.slice(:name, :options).tap { _1[:path] = section[:url] }
161
- end
162
110
  end
163
111
  end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TinyAdmin
4
+ class Store
5
+ include Utils
6
+
7
+ attr_reader :navbar, :pages, :resources, :settings
8
+
9
+ def initialize(settings)
10
+ @pages = {}
11
+ @resources = {}
12
+ @settings = settings
13
+ end
14
+
15
+ def prepare_sections(sections, logout:)
16
+ @navbar = sections.each_with_object([]) do |section, list|
17
+ unless section.is_a?(Hash)
18
+ section_class = to_class(section)
19
+ next unless section_class.respond_to?(:to_h)
20
+
21
+ section = section_class.to_h
22
+ end
23
+
24
+ slug = section[:slug].to_s
25
+ case section[:type]&.to_sym
26
+ when :content
27
+ list << add_content_section(slug, section)
28
+ when :page
29
+ list << add_page_section(slug, section)
30
+ when :resource
31
+ list << add_resource_section(slug, section)
32
+ when :url
33
+ list << add_url_section(slug, section)
34
+ end
35
+ end
36
+ navbar << logout if logout
37
+ end
38
+
39
+ private
40
+
41
+ def add_content_section(slug, section)
42
+ pages[slug] = { class: settings.content_page, content: section[:content], widgets: section[:widgets] }
43
+ TinyAdmin::Section.new(name: section[:name], slug: slug)
44
+ end
45
+
46
+ def add_page_section(slug, section)
47
+ pages[slug] = { class: to_class(section[:page]) }
48
+ TinyAdmin::Section.new(name: section[:name], slug: slug)
49
+ end
50
+
51
+ def add_resource_section(slug, section)
52
+ resource = section.slice(:resource, :only, :index, :show, :collection_actions, :member_actions)
53
+ resource[:only] ||= %i[index show]
54
+ resources[slug] = resource.merge(
55
+ model: to_class(section[:model]),
56
+ repository: to_class(section[:repository] || settings.repository)
57
+ )
58
+
59
+ hidden = section[:options] && (section[:options].include?(:hidden) || section[:options].include?('hidden'))
60
+ TinyAdmin::Section.new(name: section[:name], slug: slug) unless hidden
61
+ end
62
+
63
+ def add_url_section(slug, section)
64
+ TinyAdmin::Section.new(name: section[:name], options: section[:options], path: section[:url], slug: slug)
65
+ end
66
+ end
67
+ end
@@ -15,6 +15,10 @@ module TinyAdmin
15
15
  Kernel.format(options.first, value) if value && options&.any?
16
16
  end
17
17
 
18
+ def label_for(value, options: [])
19
+ value
20
+ end
21
+
18
22
  def round(value, options: [])
19
23
  value&.round(options&.first&.to_i || 2)
20
24
  end
@@ -14,37 +14,33 @@ module TinyAdmin
14
14
  list.join('&')
15
15
  end
16
16
 
17
- def prepare_page(page_class, options: nil)
17
+ def prepare_page(page_class, slug: nil, attributes: nil, options: nil)
18
18
  page_class.new.tap do |page|
19
19
  page.options = options
20
20
  page.head_component = TinyAdmin.settings.components[:head]&.new
21
21
  page.flash_component = TinyAdmin.settings.components[:flash]&.new
22
22
  page.navbar_component = TinyAdmin.settings.components[:navbar]&.new
23
23
  page.navbar_component&.update_attributes(
24
- current_slug: context&.slug,
24
+ current_slug: slug,
25
25
  root_path: TinyAdmin.settings.root_path,
26
26
  root_title: TinyAdmin.settings.root[:title],
27
- items: options&.include?(:no_menu) ? [] : context&.navbar
27
+ items: options&.include?(:no_menu) ? [] : TinyAdmin.settings.store&.navbar
28
28
  )
29
+ attrs = attributes || {}
30
+ attrs[:widgets] = attrs[:widgets].map { to_class(_1) } if attrs[:widgets]
31
+ page.update_attributes(attrs) unless attrs.empty?
29
32
  yield(page) if block_given?
30
33
  end
31
34
  end
32
35
 
33
- def route_for(section, reference: nil, action: nil, query: nil)
34
- root_path = TinyAdmin.settings.root_path == '/' ? nil : TinyAdmin.settings.root_path
35
- route = [root_path, section, reference, action].compact.join("/")
36
- route << "?#{query}" if query
37
- route[0] == '/' ? route : route.prepend('/')
36
+ def to_class(klass)
37
+ klass.is_a?(String) ? Object.const_get(klass) : klass
38
38
  end
39
39
 
40
- def to_label(string)
40
+ def humanize(string)
41
41
  return '' unless string
42
42
 
43
43
  string.respond_to?(:humanize) ? string.humanize : string.tr('_', ' ').capitalize
44
44
  end
45
-
46
- def context
47
- TinyAdmin::Context.instance
48
- end
49
45
  end
50
46
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module TinyAdmin
4
- VERSION = '0.6.0'
4
+ VERSION = '0.8.0'
5
5
  end
@@ -4,14 +4,16 @@ module TinyAdmin
4
4
  module Views
5
5
  module Actions
6
6
  class Index < DefaultLayout
7
- attr_accessor :actions, :fields, :filters, :links, :pagination_component, :prepare_record, :records
7
+ attr_accessor :actions, :fields, :filters, :links, :pagination_component, :prepare_record, :records, :slug
8
8
 
9
9
  def template
10
10
  super do
11
11
  div(class: 'index') {
12
12
  div(class: 'row') {
13
13
  div(class: 'col-4') {
14
- h1(class: 'title') { title }
14
+ h1(class: 'title') {
15
+ title
16
+ }
15
17
  }
16
18
  div(class: 'col-8') {
17
19
  actions_buttons
@@ -31,13 +33,15 @@ module TinyAdmin
31
33
  if filters&.any?
32
34
  div(class: 'col-3') {
33
35
  filters_form = TinyAdmin::Views::Components::FiltersForm.new
34
- filters_form.update_attributes(section_path: route_for(context.slug), filters: filters)
36
+ filters_form.update_attributes(section_path: TinyAdmin.route_for(slug), filters: filters)
35
37
  render filters_form
36
38
  }
37
39
  end
38
40
  }
39
41
 
40
42
  render pagination_component if pagination_component
43
+
44
+ render TinyAdmin::Views::Components::Widgets.new(widgets)
41
45
  }
42
46
  end
43
47
  end
@@ -65,13 +69,7 @@ module TinyAdmin
65
69
  attributes.each do |key, value|
66
70
  field = fields[key]
67
71
  td(class: "field-value-#{field.name} field-value-type-#{field.type}") {
68
- if field.options && field.options[:link_to]
69
- a(href: route_for(field.options[:link_to], reference: value)) {
70
- field.apply_call_option(record) || value
71
- }
72
- else
73
- value
74
- end
72
+ render TinyAdmin.settings.components[:field_value].new(field, value, record: record)
75
73
  }
76
74
  end
77
75
 
@@ -82,15 +80,20 @@ module TinyAdmin
82
80
  links.each do |link|
83
81
  whitespace
84
82
  if link == 'show'
85
- a(href: route_for(context.slug, reference: record.id), class: link_class) { 'show' }
83
+ a(href: TinyAdmin.route_for(slug, reference: record.id), class: link_class) {
84
+ label_for('Show', options: ['actions.index.links.show'])
85
+ }
86
86
  else
87
- a(href: route_for(context.slug, reference: record.id, action: link), class: link_class) {
88
- to_label(link)
87
+ a(href: TinyAdmin.route_for(slug, reference: record.id, action: link), class: link_class) {
88
+ fallback = humanize(link)
89
+ label_for(fallback, options: ["actions.index.links.#{link}"])
89
90
  }
90
91
  end
91
92
  end
92
93
  else
93
- a(href: route_for(context.slug, reference: record.id), class: link_class) { 'show' }
94
+ a(href: TinyAdmin.route_for(slug, reference: record.id), class: link_class) {
95
+ label_for('Show', options: ['actions.index.links.show'])
96
+ }
94
97
  end
95
98
  }
96
99
  }
@@ -103,7 +106,7 @@ module TinyAdmin
103
106
  ul(class: 'nav justify-content-end') {
104
107
  (actions || {}).each do |action, action_class|
105
108
  li(class: 'nav-item mx-1') {
106
- href = route_for(context.slug, action: action)
109
+ href = TinyAdmin.route_for(slug, action: action)
107
110
  a(href: href, class: 'nav-link btn btn-outline-secondary') {
108
111
  action_class.respond_to?(:title) ? action_class.title : action
109
112
  }
@@ -4,7 +4,7 @@ module TinyAdmin
4
4
  module Views
5
5
  module Actions
6
6
  class Show < DefaultLayout
7
- attr_accessor :actions, :fields, :prepare_record, :record
7
+ attr_accessor :actions, :fields, :prepare_record, :record, :reference, :slug
8
8
 
9
9
  def template
10
10
  super do
@@ -25,16 +25,12 @@ module TinyAdmin
25
25
  div(class: 'field-header col-2') { field.options[:header] || field.title }
26
26
  end
27
27
  div(class: 'field-value col-10') {
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
- }
32
- else
33
- value
34
- end
28
+ render TinyAdmin.settings.components[:field_value].new(field, value, record: record)
35
29
  }
36
30
  }
37
31
  end
32
+
33
+ render TinyAdmin::Views::Components::Widgets.new(widgets)
38
34
  }
39
35
  end
40
36
  end
@@ -45,7 +41,7 @@ module TinyAdmin
45
41
  ul(class: 'nav justify-content-end') {
46
42
  (actions || {}).each do |action, action_class|
47
43
  li(class: 'nav-item mx-1') {
48
- href = route_for(context.slug, reference: context.reference, action: action)
44
+ href = TinyAdmin.route_for(slug, reference: reference, action: action)
49
45
  a(href: href, class: 'nav-link btn btn-outline-secondary') {
50
46
  action_class.respond_to?(:title) ? action_class.title : action
51
47
  }
@@ -5,7 +5,11 @@ module TinyAdmin
5
5
  class BasicLayout < Phlex::HTML
6
6
  include Utils
7
7
 
8
- attr_accessor :content
8
+ attr_accessor :content, :widgets
9
+
10
+ def label_for(value, options: [])
11
+ TinyAdmin.settings.helper_class.label_for(value, options: options)
12
+ end
9
13
 
10
14
  def update_attributes(attributes)
11
15
  attributes.each do |key, value|
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TinyAdmin
4
+ module Views
5
+ class BasicWidget < Phlex::HTML
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TinyAdmin
4
+ module Views
5
+ module Components
6
+ class FieldValue < BasicComponent
7
+ attr_reader :field, :value, :record
8
+
9
+ def initialize(field, value, record:)
10
+ @field = field
11
+ @value = value
12
+ @record = record
13
+ end
14
+
15
+ def template
16
+ translated_value = field.translate_value(value)
17
+ value_class = field.options[:options]&.include?('value_class') ? "value-#{value}" : nil
18
+ if field.options[:link_to]
19
+ a(href: TinyAdmin.route_for(field.options[:link_to], reference: translated_value)) {
20
+ span(class: value_class) {
21
+ field.apply_call_option(record) || translated_value
22
+ }
23
+ }
24
+ else
25
+ span(class: value_class) {
26
+ translated_value
27
+ }
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -17,8 +17,12 @@ module TinyAdmin
17
17
  when :boolean
18
18
  select(class: 'form-select', id: "filter-#{name}", name: "q[#{name}]") {
19
19
  option(value: '') { '-' }
20
- option(value: '0', selected: filter[:value] == '0') { 'false' }
21
- option(value: '1', selected: filter[:value] == '1') { 'true' }
20
+ option(value: '0', selected: filter[:value] == '0') {
21
+ TinyAdmin.settings.helper_class.label_for('false', options: ['components.filters_form.boolean.false'])
22
+ }
23
+ option(value: '1', selected: filter[:value] == '1') {
24
+ TinyAdmin.settings.helper_class.label_for('true', options: ['components.filters_form.boolean.true'])
25
+ }
22
26
  }
23
27
  when :date
24
28
  input(type: 'date', class: 'form-control', id: "filter-#{name}", name: "q[#{name}]", value: filter[:value])
@@ -40,9 +44,13 @@ module TinyAdmin
40
44
  end
41
45
 
42
46
  div(class: 'mt-3') {
43
- a(href: section_path, class: 'button_clear btn btn-secondary text-white') { 'clear' }
47
+ a(href: section_path, class: 'button_clear btn btn-secondary text-white') {
48
+ TinyAdmin.settings.helper_class.label_for('Clear', options: ['components.filters_form.buttons.clear'])
49
+ }
44
50
  whitespace
45
- button(type: 'submit', class: 'button_filter btn btn-secondary') { 'filter' }
51
+ button(type: 'submit', class: 'button_filter btn btn-secondary') {
52
+ TinyAdmin.settings.helper_class.label_for('Filter', options: ['components.filters_form.buttons.submit'])
53
+ }
46
54
  }
47
55
  }
48
56
  end
@@ -23,14 +23,14 @@ module TinyAdmin
23
23
  }
24
24
  div(class: 'collapse navbar-collapse', id: 'navbarNav') {
25
25
  ul(class: 'navbar-nav') {
26
- items.each do |slug, item|
26
+ items.each do |item|
27
27
  classes = %w[nav-link]
28
- classes << 'active' if slug == current_slug
29
- link_attributes = { class: classes.join(' '), href: item[:path], 'aria-current' => 'page' }
30
- link_attributes.merge!(item[:options]) if item[:options]
28
+ classes << 'active' if item.slug == current_slug
29
+ link_attributes = { class: classes.join(' '), href: item.path, 'aria-current' => 'page' }
30
+ link_attributes.merge!(item.options) if item.options
31
31
 
32
32
  li(class: 'nav-item') {
33
- a(**link_attributes) { item[:name] }
33
+ a(**link_attributes) { item.name }
34
34
  }
35
35
  end
36
36
  }
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TinyAdmin
4
+ module Views
5
+ module Components
6
+ class Widgets < BasicComponent
7
+ def initialize(widgets)
8
+ @widgets = widgets
9
+ end
10
+
11
+ def template
12
+ return if @widgets.nil? || @widgets.empty?
13
+
14
+ div(class: 'container widgets') {
15
+ @widgets.each_slice(2).each do |row|
16
+ div(class: 'row') {
17
+ row.each do |widget|
18
+ next unless widget < Phlex::HTML
19
+
20
+ div(class: 'col') {
21
+ div(class: 'card') {
22
+ div(class: 'card-body') {
23
+ render widget.new
24
+ }
25
+ }
26
+ }
27
+ end
28
+ }
29
+ end
30
+ }
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -7,7 +7,11 @@ module TinyAdmin
7
7
  def template
8
8
  super do
9
9
  div(class: 'content') {
10
- unsafe_raw(content)
10
+ div(class: 'content-data') {
11
+ unsafe_raw(content)
12
+ }
13
+
14
+ render TinyAdmin::Views::Components::Widgets.new(widgets)
11
15
  }
12
16
  end
13
17
  end
@@ -7,7 +7,7 @@ module TinyAdmin
7
7
  def template
8
8
  super do
9
9
  div(class: 'root') {
10
- h1(class: 'title') { 'Tiny Admin' }
10
+ render TinyAdmin::Views::Components::Widgets.new(widgets)
11
11
  }
12
12
  end
13
13
  end
@@ -11,12 +11,16 @@ module TinyAdmin
11
11
 
12
12
  form(class: 'form_login', method: 'post') {
13
13
  div(class: 'mt-3') {
14
- label(for: 'secret', class: 'form-label') { 'Password' }
14
+ label(for: 'secret', class: 'form-label') {
15
+ label_for('Password', options: ['pages.simple_auth_login.inputs.password'])
16
+ }
15
17
  input(type: 'password', name: 'secret', class: 'form-control', id: 'secret')
16
18
  }
17
19
 
18
20
  div(class: 'mt-3') {
19
- button(type: 'submit', class: 'button_login btn btn-primary') { 'login' }
21
+ button(type: 'submit', class: 'button_login btn btn-primary') {
22
+ label_for('Login', options: ['pages.simple_auth_login.buttons.submit'])
23
+ }
20
24
  }
21
25
  }
22
26
  }
data/lib/tiny_admin.rb CHANGED
@@ -4,6 +4,7 @@ require 'phlex'
4
4
  require 'roda'
5
5
  require 'zeitwerk'
6
6
 
7
+ require 'forwardable'
7
8
  require 'singleton'
8
9
  require 'yaml'
9
10
 
@@ -23,9 +24,16 @@ module TinyAdmin
23
24
  end
24
25
  end
25
26
 
27
+ def route_for(section, reference: nil, action: nil, query: nil)
28
+ root_path = settings.root_path == '/' ? nil : settings.root_path
29
+ route = [root_path, section, reference, action].compact.join("/")
30
+ route << "?#{query}" if query
31
+ route[0] == '/' ? route : route.prepend('/')
32
+ end
33
+
26
34
  def settings
27
35
  TinyAdmin::Settings.instance
28
36
  end
29
37
 
30
- module_function :configure, :configure_from_file, :settings
38
+ module_function :configure, :configure_from_file, :route_for, :settings
31
39
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: tiny_admin
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.0
4
+ version: 0.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mattia Roccoberton
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-04-28 00:00:00.000000000 Z
11
+ date: 2023-05-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: phlex
@@ -87,19 +87,24 @@ files:
87
87
  - lib/tiny_admin/plugins/no_auth.rb
88
88
  - lib/tiny_admin/plugins/simple_auth.rb
89
89
  - lib/tiny_admin/router.rb
90
+ - lib/tiny_admin/section.rb
90
91
  - lib/tiny_admin/settings.rb
92
+ - lib/tiny_admin/store.rb
91
93
  - lib/tiny_admin/support.rb
92
94
  - lib/tiny_admin/utils.rb
93
95
  - lib/tiny_admin/version.rb
94
96
  - lib/tiny_admin/views/actions/index.rb
95
97
  - lib/tiny_admin/views/actions/show.rb
96
98
  - lib/tiny_admin/views/basic_layout.rb
99
+ - lib/tiny_admin/views/basic_widget.rb
97
100
  - lib/tiny_admin/views/components/basic_component.rb
101
+ - lib/tiny_admin/views/components/field_value.rb
98
102
  - lib/tiny_admin/views/components/filters_form.rb
99
103
  - lib/tiny_admin/views/components/flash.rb
100
104
  - lib/tiny_admin/views/components/head.rb
101
105
  - lib/tiny_admin/views/components/navbar.rb
102
106
  - lib/tiny_admin/views/components/pagination.rb
107
+ - lib/tiny_admin/views/components/widgets.rb
103
108
  - lib/tiny_admin/views/default_layout.rb
104
109
  - lib/tiny_admin/views/pages/content.rb
105
110
  - lib/tiny_admin/views/pages/page_not_found.rb