tiny_admin 0.6.0 → 0.8.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.
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