tiny_admin 0.5.0 → 0.7.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: c3ac2be067ca35c35f40c677c5f9461c5f83e366488a48e6ab50a888f0f22317
4
- data.tar.gz: 2289dd33940174e8e614260f96273967ade51f75dc67a04d5e639415c984e65b
3
+ metadata.gz: d8059ea24a18bfb14f8e868a70203db8045099e20196f5098f1c63b121421e49
4
+ data.tar.gz: d0da80d718362c58a50e963b2e79dff550e77a763d7872a0ba96efea43f408b3
5
5
  SHA512:
6
- metadata.gz: f4f80d86ac6b30bc29cf8e2f1efe480edefe84907bf5e9b7a3a8c3b3c3750ff59a1f9e609c87090105c0cd31fa22df7aa913858911a455d918633269c27317e7
7
- data.tar.gz: 4fe8a75d9e3c0cc3bd756ab247fecb7575049dcec58559bdb7cdb1c58ecb4b6e7de3f8be96c396b0b304134cc75c6058b3cbeafce92cd027413971a99a812c02
6
+ metadata.gz: 3067f890f10f10f5dc7379c7b147f01f5cb62b8d59f06ad7b4387c685cd2e32cdeb1f87fbfe36b0e2c388c854ce461f1a18b2659ee479e401ba74ed3c5559a3a
7
+ data.tar.gz: 0a4b4129b33e0dbf2ab2d01df14ead0ec3971a098dbed39a5b50c287e2a00c5c8448011de81a233c7bb1dda58bb42b648d44069684a65cdb4e42e3faf2a453bb
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 Rails 7.0](https://github.com/blocknotes/tiny_admin/actions/workflows/specs_rails_70.yml/badge.svg)](https://github.com/blocknotes/tiny_admin/actions/workflows/specs_rails_70.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.5'`
22
+ - Add to your Gemfile: `gem 'tiny_admin', '~> 0.7'`
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):
@@ -51,6 +54,7 @@ Plugin available:
51
54
  Pages available:
52
55
 
53
56
  - **Root**: define how to present the content in the main page of the interface;
57
+ - **Content**: define how to present page with inline content;
54
58
  - **PageNotFound**: define how to present pages not found;
55
59
  - **RecordNotFound**: define how to present record not found page;
56
60
  - **SimpleAuthLogin**: define how to present the login form for SimpleAuth plugin;
@@ -79,6 +83,7 @@ The following options are supported:
79
83
  - `title` (String): root section's title;
80
84
  - `page` (String): a view object to render;
81
85
  - `redirect` (String): alternative to _page_ option - redirects to a specific slug;
86
+ - `widgets` (Array): list of widgets (as View components) to present.
82
87
 
83
88
  Example:
84
89
 
@@ -86,6 +91,9 @@ Example:
86
91
  root:
87
92
  title: MyAdmin
88
93
  redirect: posts
94
+ widgets:
95
+ - LatestAuthorsWidget
96
+ - LatestPostsWidget
89
97
  ```
90
98
 
91
99
  `helper_class` (String): class or module with helper methods, used for attributes' formatters.
@@ -122,9 +130,28 @@ authentication:
122
130
 
123
131
  - `slug` (String): section reference identifier;
124
132
  - `name` (String): section's title;
125
- - `type` (String): the type of section: `url`, `page` or `resource`;
133
+ - `type` (String): the type of section: `content`, `page`, `resource` or `url`;
134
+ - `widgets` (Array): list of widgets (as View components) to present;
126
135
  - other properties depends on the section's type.
127
136
 
137
+ For _content_ sections:
138
+
139
+ - `content` (String): the HTML content to present.
140
+
141
+ Example:
142
+
143
+ ```yml
144
+ slug: test-content
145
+ name: Test content
146
+ type: content
147
+ content: >
148
+ <h1>Test content!</h1>
149
+ <p>Some test content</p>
150
+ widgets:
151
+ - LatestAuthorsWidget
152
+ - LatestPostsWidget
153
+ ```
154
+
128
155
  For _url_ sections:
129
156
 
130
157
  - `url` (String): the URL to load when clicking on the section's menu item;
@@ -163,6 +190,7 @@ For _resource_ sections:
163
190
  - `show` (Hash): detail's action options (see below);
164
191
  - `collection_actions` (Array of hashes): custom collection's actions;
165
192
  - `member_actions` (Array of hashes): custom details's actions;
193
+ - `widgets` (Array): list of widgets (as View components) to present;
166
194
  - `only` (Array of strings): list of supported actions (ex. `index`);
167
195
  - `options` (Array of strings): resource options (ex. `hidden`).
168
196
 
@@ -240,6 +268,9 @@ Example:
240
268
  header: The author
241
269
  link_to: authors
242
270
  call: author, name
271
+ widgets:
272
+ - LatestAuthorsWidget
273
+ - LatestPostsWidget
243
274
  ```
244
275
 
245
276
  ### Sample
@@ -264,6 +295,9 @@ authentication:
264
295
  # password: 'f1891cea80fc05e433c943254c6bdabc159577a02a7395dfebbfbc4f7661d4af56f2d372131a45936de40160007368a56ef216a30cb202c66d3145fd24380906'
265
296
  root:
266
297
  title: Test Admin
298
+ widgets:
299
+ - LatestAuthorsWidget
300
+ - LatestPostsWidget
267
301
  # page: RootPage
268
302
  helper_class: AdminHelper
269
303
  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, 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
@@ -5,7 +5,7 @@ module TinyAdmin
5
5
  route do |r|
6
6
  r.get 'unauthenticated' do
7
7
  if current_user
8
- r.redirect settings.root_path
8
+ r.redirect TinyAdmin.settings.root_path
9
9
  else
10
10
  render_login
11
11
  end
@@ -17,14 +17,17 @@ module TinyAdmin
17
17
 
18
18
  r.get 'logout' do
19
19
  logout_user
20
- r.redirect settings.root_path
20
+ r.redirect TinyAdmin.settings.root_path
21
21
  end
22
22
  end
23
23
 
24
24
  private
25
25
 
26
26
  def render_login(notices: nil, warnings: nil, errors: nil)
27
- page = prepare_page(settings.authentication[:login], options: %i[no_menu compact_layout])
27
+ login = TinyAdmin.settings.authentication[:login]
28
+ return unless login
29
+
30
+ page = prepare_page(login, options: %i[no_menu compact_layout])
28
31
  page.messages = {
29
32
  notices: notices || flash['notices'],
30
33
  warnings: warnings || flash['warnings'],
@@ -6,7 +6,7 @@ module TinyAdmin
6
6
 
7
7
  class << self
8
8
  def authentication_plugin
9
- plugin = TinyAdmin::Settings.instance.authentication&.dig(:plugin)
9
+ plugin = TinyAdmin.settings.authentication&.dig(:plugin)
10
10
  plugin_class = plugin.is_a?(String) ? Object.const_get(plugin) : plugin
11
11
  plugin_class || TinyAdmin::Plugins::NoAuth
12
12
  end
@@ -17,8 +17,8 @@ module TinyAdmin
17
17
  plugin :render, engine: 'html'
18
18
  plugin :sessions, secret: SecureRandom.hex(64)
19
19
 
20
- plugin authentication_plugin, TinyAdmin::Settings.instance.authentication
20
+ plugin authentication_plugin, TinyAdmin.settings.authentication
21
21
 
22
- not_found { prepare_page(settings.page_not_found).call }
22
+ not_found { prepare_page(TinyAdmin.settings.page_not_found).call }
23
23
  end
24
24
  end
@@ -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
@@ -20,12 +20,12 @@ module TinyAdmin
20
20
  def fields(options: nil)
21
21
  if options
22
22
  types = model.columns.to_h { [_1.name, _1.type] }
23
- options.each_with_object({}) do |(name, field_options), result|
24
- result[name] = TinyAdmin::Field.create_field(name: name, type: types[name], options: field_options)
23
+ options.to_h do |name, field_options|
24
+ [name, TinyAdmin::Field.create_field(name: name, type: types[name], options: field_options)]
25
25
  end
26
26
  else
27
- model.columns.each_with_object({}) do |column, result|
28
- result[column.name] = TinyAdmin::Field.create_field(name: column.name, type: column.type)
27
+ model.columns.to_h do |column|
28
+ [column.name, TinyAdmin::Field.create_field(name: column.name, type: column.type)]
29
29
  end
30
30
  end
31
31
  end
@@ -42,8 +42,12 @@ module TinyAdmin
42
42
  raise BaseRepository::RecordNotFound, e.message
43
43
  end
44
44
 
45
+ def collection
46
+ model.all
47
+ end
48
+
45
49
  def list(page: 1, limit: 10, sort: nil, filters: nil)
46
- query = sort ? model.all.order(sort) : model.all
50
+ query = sort ? collection.order(sort) : collection
47
51
  query = apply_filters(query, filters) if filters
48
52
  page_offset = page.positive? ? (page - 1) * limit : 0
49
53
  records = query.offset(page_offset).limit(limit).to_a
@@ -18,7 +18,7 @@ module TinyAdmin
18
18
  converter = Object.const_get(field[:converter])
19
19
  converter.send(method, value, options: options || [])
20
20
  else
21
- Settings.instance.helper_class.send(method, value, options: options || [])
21
+ TinyAdmin.settings.helper_class.send(method, value, options: options || [])
22
22
  end
23
23
  else
24
24
  value&.to_s
@@ -2,13 +2,14 @@
2
2
 
3
3
  module TinyAdmin
4
4
  class Router < BasicApp
5
+ extend Forwardable
6
+
7
+ def_delegator TinyAdmin, :route_for
8
+
5
9
  route do |r|
6
- context.settings = TinyAdmin::Settings.instance
7
- context.settings.load_settings
8
- context.router = r
10
+ TinyAdmin.settings.load_settings
9
11
 
10
12
  r.on 'auth' do
11
- context.slug = nil
12
13
  r.run Authentication
13
14
  end
14
15
 
@@ -25,15 +26,14 @@ module TinyAdmin
25
26
  end
26
27
 
27
28
  r.post '' do
28
- context.slug = nil
29
- r.redirect settings.root_path
29
+ r.redirect TinyAdmin.settings.root_path
30
30
  end
31
31
 
32
- context.pages.each do |slug, data|
33
- setup_page_route(r, slug, data)
32
+ store.pages.each do |slug, page_data|
33
+ setup_page_route(r, slug, page_data)
34
34
  end
35
35
 
36
- context.resources.each do |slug, options|
36
+ store.resources.each do |slug, options|
37
37
  setup_resource_routes(r, slug, options: options || {})
38
38
  end
39
39
 
@@ -42,6 +42,10 @@ module TinyAdmin
42
42
 
43
43
  private
44
44
 
45
+ def store
46
+ @store ||= TinyAdmin.settings.store
47
+ end
48
+
45
49
  def render_page(page)
46
50
  if page.respond_to?(:messages=)
47
51
  page.messages = { notices: flash['notices'], warnings: flash['warnings'], errors: flash['errors'] }
@@ -50,74 +54,83 @@ module TinyAdmin
50
54
  end
51
55
 
52
56
  def root_route(router)
53
- context.slug = nil
54
- if settings.root[:redirect]
55
- router.redirect route_for(settings.root[:redirect])
57
+ if TinyAdmin.settings.root[:redirect]
58
+ router.redirect route_for(TinyAdmin.settings.root[:redirect])
56
59
  else
57
- page = settings.root[:page]
58
- page_class = page.is_a?(String) ? Object.const_get(page) : page
59
- 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))
60
62
  end
61
63
  end
62
64
 
63
- def setup_page_route(router, slug, page_class)
65
+ def setup_page_route(router, slug, page_data)
64
66
  router.get slug do
65
- context.slug = slug
66
- render_page prepare_page(page_class)
67
+ attributes = page_data.slice(:content, :title, :widgets)
68
+ render_page prepare_page(page_data[:class], slug: slug, attributes: attributes)
67
69
  end
68
70
  end
69
71
 
70
72
  def setup_resource_routes(router, slug, options:)
71
73
  router.on slug do
72
- context.slug = slug
73
- setup_collection_routes(router, options: options)
74
- setup_member_routes(router, options: options)
74
+ setup_collection_routes(router, slug, options: options)
75
+ setup_member_routes(router, slug, options: options)
75
76
  end
76
77
  end
77
78
 
78
- def setup_collection_routes(router, options:)
79
- context.repository = options[:repository].new(options[:model])
79
+ def setup_collection_routes(router, slug, options:)
80
+ repository = options[:repository].new(options[:model])
80
81
  action_options = options[:index] || {}
81
82
 
82
83
  # Custom actions
83
84
  custom_actions = setup_custom_actions(
84
85
  router,
85
86
  options[:collection_actions],
86
- repository: context.repository,
87
- options: action_options
87
+ options: action_options,
88
+ repository: repository,
89
+ slug: slug
88
90
  )
89
91
 
90
92
  # Index
91
93
  if options[:only].include?(:index) || options[:only].include?('index')
92
94
  router.is do
93
- context.actions = custom_actions
94
- context.request = request
95
+ context = Context.new(
96
+ actions: custom_actions,
97
+ repository: repository,
98
+ request: request,
99
+ router: router,
100
+ slug: slug
101
+ )
95
102
  index_action = TinyAdmin::Actions::Index.new
96
103
  render_page index_action.call(app: self, context: context, options: action_options)
97
104
  end
98
105
  end
99
106
  end
100
107
 
101
- def setup_member_routes(router, options:)
102
- context.repository = options[:repository].new(options[:model])
103
- action_options = (options[:show] || {}).merge(record_not_found_page: settings.record_not_found)
108
+ def setup_member_routes(router, slug, options:)
109
+ repository = options[:repository].new(options[:model])
110
+ action_options = (options[:show] || {}).merge(record_not_found_page: TinyAdmin.settings.record_not_found)
104
111
 
105
112
  router.on String do |reference|
106
- context.reference = reference
107
-
108
113
  # Custom actions
109
114
  custom_actions = setup_custom_actions(
110
115
  router,
111
116
  options[:member_actions],
112
- repository: context.repository,
113
- options: action_options
117
+ options: action_options,
118
+ repository: repository,
119
+ slug: slug,
120
+ reference: reference
114
121
  )
115
122
 
116
123
  # Show
117
124
  if options[:only].include?(:show) || options[:only].include?('show')
118
125
  router.is do
119
- context.actions = custom_actions
120
- 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
+ )
121
134
  show_action = TinyAdmin::Actions::Show.new
122
135
  render_page show_action.call(app: self, context: context, options: action_options)
123
136
  end
@@ -125,15 +138,20 @@ module TinyAdmin
125
138
  end
126
139
  end
127
140
 
128
- def setup_custom_actions(router, custom_actions, repository:, options:)
129
- context.repository = repository
141
+ def setup_custom_actions(router, custom_actions, options:, repository:, slug:, reference: nil)
130
142
  (custom_actions || []).each_with_object({}) do |custom_action, result|
131
143
  action_slug, action = custom_action.first
132
- action_class = action.is_a?(String) ? Object.const_get(action) : action
144
+ action_class = to_class(action)
133
145
 
134
146
  router.get action_slug.to_s do
135
- context.actions = {}
136
- 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
+ )
137
155
  custom_action = action_class.new
138
156
  render_page custom_action.call(app: self, context: context, options: options)
139
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,7 +3,6 @@
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,
@@ -12,34 +11,53 @@ module TinyAdmin
12
11
  %i[components head] => Views::Components::Head,
13
12
  %i[components navbar] => Views::Components::Navbar,
14
13
  %i[components pagination] => Views::Components::Pagination,
14
+ %i[content_page] => Views::Pages::Content,
15
15
  %i[helper_class] => Support,
16
16
  %i[page_not_found] => Views::Pages::PageNotFound,
17
17
  %i[record_not_found] => Views::Pages::RecordNotFound,
18
18
  %i[repository] => Plugins::ActiveRecordRepository,
19
19
  %i[root_path] => '/admin',
20
20
  %i[root page] => Views::Pages::Root,
21
- %i[root title] => 'TinyAdmin'
21
+ %i[root title] => 'TinyAdmin',
22
+ %i[sections] => []
22
23
  }.freeze
23
24
 
24
- attr_accessor :authentication,
25
- :components,
26
- :extra_styles,
27
- :helper_class,
28
- :page_not_found,
29
- :record_not_found,
30
- :repository,
31
- :root,
32
- :root_path,
33
- :sections,
34
- :scripts,
35
- :style_links
25
+ OPTIONS = %i[
26
+ authentication
27
+ components
28
+ content_page
29
+ extra_styles
30
+ helper_class
31
+ page_not_found
32
+ record_not_found
33
+ repository
34
+ root
35
+ root_path
36
+ sections
37
+ scripts
38
+ style_links
39
+ ].freeze
40
+
41
+ attr_reader :store
42
+
43
+ OPTIONS.each do |option|
44
+ define_method(option) do
45
+ self[option]
46
+ end
47
+
48
+ define_method("#{option}=") do |value|
49
+ self[option] = value
50
+ end
51
+ end
36
52
 
37
- def [](key)
38
- send(key)
53
+ def [](*path)
54
+ key, option = fetch_setting(path)
55
+ option[key]
39
56
  end
40
57
 
41
- def []=(key, value)
42
- send("#{key}=", value)
58
+ def []=(*path, value)
59
+ key, option = fetch_setting(path)
60
+ option[key] = value
43
61
  convert_value(key, value)
44
62
  end
45
63
 
@@ -54,24 +72,33 @@ module TinyAdmin
54
72
  end
55
73
  end
56
74
 
57
- context.pages ||= {}
58
- context.resources ||= {}
59
- @sections ||= []
60
- @root_path = '/' if @root_path == ''
75
+ @store ||= TinyAdmin::Store.new(self)
76
+ self.root_path = '/' if root_path == ''
61
77
 
62
- if @authentication[:plugin] <= Plugins::SimpleAuth
63
- @authentication[:logout] ||= { name: 'logout', path: "#{root_path}/auth/logout" }
78
+ if authentication[:plugin] <= Plugins::SimpleAuth
79
+ logout_path = "#{root_path}/auth/logout"
80
+ authentication[:logout] ||= TinyAdmin::Section.new(name: 'logout', slug: 'logout', path: logout_path)
64
81
  end
65
- context.navbar = prepare_navbar(sections, logout: authentication[:logout])
82
+ store.prepare_sections(sections, logout: authentication[:logout])
83
+ end
84
+
85
+ def reset!
86
+ @options = {}
66
87
  end
67
88
 
68
89
  private
69
90
 
91
+ def fetch_setting(path)
92
+ @options ||= {}
93
+ *parts, last = path.map(&:to_sym)
94
+ [last, parts.inject(@options) { |result, part| result[part] ||= {} }]
95
+ end
96
+
70
97
  def convert_value(key, value)
71
98
  if value.is_a?(Hash)
72
99
  value.each_key do |key2|
73
100
  path = [key, key2]
74
- if DEFAULTS[path].is_a?(Class) || DEFAULTS[path].is_a?(Module)
101
+ if (DEFAULTS[path].is_a?(Class) || DEFAULTS[path].is_a?(Module)) && self[key][key2].is_a?(String)
75
102
  self[key][key2] = Object.const_get(self[key][key2])
76
103
  end
77
104
  end
@@ -79,51 +106,5 @@ module TinyAdmin
79
106
  self[key] = Object.const_get(self[key])
80
107
  end
81
108
  end
82
-
83
- def prepare_navbar(sections, logout:)
84
- items = sections.each_with_object({}) do |section, list|
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
93
- case section[:type]&.to_sym
94
- when :url
95
- list[slug] = add_url_section(slug, section)
96
- when :page
97
- list[slug] = add_page_section(slug, section)
98
- when :resource
99
- list[slug] = add_resource_section(slug, section)
100
- end
101
- end
102
- items['auth/logout'] = logout if logout
103
- items
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
128
109
  end
129
110
  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
@@ -14,27 +14,27 @@ 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
- page.head_component = settings.components[:head]&.new
21
- page.flash_component = settings.components[:flash]&.new
22
- page.navbar_component = settings.components[:navbar]&.new
20
+ page.head_component = TinyAdmin.settings.components[:head]&.new
21
+ page.flash_component = TinyAdmin.settings.components[:flash]&.new
22
+ page.navbar_component = TinyAdmin.settings.components[:navbar]&.new
23
23
  page.navbar_component&.update_attributes(
24
- current_slug: context&.slug,
25
- root_path: settings.root_path,
26
- root_title: settings.root[:title],
27
- items: options&.include?(:no_menu) ? [] : context&.navbar
24
+ current_slug: slug,
25
+ root_path: TinyAdmin.settings.root_path,
26
+ root_title: TinyAdmin.settings.root[:title],
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 = settings.root_path == '/' ? nil : 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
40
  def to_label(string)
@@ -42,13 +42,5 @@ module TinyAdmin
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
-
50
- def settings
51
- TinyAdmin::Settings.instance
52
- end
53
45
  end
54
46
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module TinyAdmin
4
- VERSION = '0.5.0'
4
+ VERSION = '0.7.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
@@ -66,7 +70,7 @@ module TinyAdmin
66
70
  field = fields[key]
67
71
  td(class: "field-value-#{field.name} field-value-type-#{field.type}") {
68
72
  if field.options && field.options[:link_to]
69
- a(href: route_for(field.options[:link_to], reference: value)) {
73
+ a(href: TinyAdmin.route_for(field.options[:link_to], reference: value)) {
70
74
  field.apply_call_option(record) || value
71
75
  }
72
76
  else
@@ -82,15 +86,15 @@ module TinyAdmin
82
86
  links.each do |link|
83
87
  whitespace
84
88
  if link == 'show'
85
- a(href: route_for(context.slug, reference: record.id), class: link_class) { 'show' }
89
+ a(href: TinyAdmin.route_for(slug, reference: record.id), class: link_class) { 'show' }
86
90
  else
87
- a(href: route_for(context.slug, reference: record.id, action: link), class: link_class) {
91
+ a(href: TinyAdmin.route_for(slug, reference: record.id, action: link), class: link_class) {
88
92
  to_label(link)
89
93
  }
90
94
  end
91
95
  end
92
96
  else
93
- a(href: route_for(context.slug, reference: record.id), class: link_class) { 'show' }
97
+ a(href: TinyAdmin.route_for(slug, reference: record.id), class: link_class) { 'show' }
94
98
  end
95
99
  }
96
100
  }
@@ -103,7 +107,7 @@ module TinyAdmin
103
107
  ul(class: 'nav justify-content-end') {
104
108
  (actions || {}).each do |action, action_class|
105
109
  li(class: 'nav-item mx-1') {
106
- href = route_for(context.slug, action: action)
110
+ href = TinyAdmin.route_for(slug, action: action)
107
111
  a(href: href, class: 'nav-link btn btn-outline-secondary') {
108
112
  action_class.respond_to?(:title) ? action_class.title : action
109
113
  }
@@ -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
@@ -26,7 +26,7 @@ module TinyAdmin
26
26
  end
27
27
  div(class: 'field-value col-10') {
28
28
  if field.options[:link_to]
29
- a(href: route_for(field.options[:link_to], reference: value)) {
29
+ a(href: TinyAdmin.route_for(field.options[:link_to], reference: value)) {
30
30
  field.apply_call_option(record) || value
31
31
  }
32
32
  else
@@ -35,6 +35,8 @@ module TinyAdmin
35
35
  }
36
36
  }
37
37
  end
38
+
39
+ render TinyAdmin::Views::Components::Widgets.new(widgets)
38
40
  }
39
41
  end
40
42
  end
@@ -45,7 +47,7 @@ module TinyAdmin
45
47
  ul(class: 'nav justify-content-end') {
46
48
  (actions || {}).each do |action, action_class|
47
49
  li(class: 'nav-item mx-1') {
48
- href = route_for(context.slug, reference: context.reference, action: action)
50
+ href = TinyAdmin.route_for(slug, reference: reference, action: action)
49
51
  a(href: href, class: 'nav-link btn btn-outline-secondary') {
50
52
  action_class.respond_to?(:title) ? action_class.title : action
51
53
  }
@@ -5,6 +5,8 @@ module TinyAdmin
5
5
  class BasicLayout < Phlex::HTML
6
6
  include Utils
7
7
 
8
+ attr_accessor :content, :widgets
9
+
8
10
  def update_attributes(attributes)
9
11
  attributes.each do |key, value|
10
12
  send("#{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
@@ -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
@@ -6,7 +6,7 @@ module TinyAdmin
6
6
  attr_accessor :flash_component, :head_component, :messages, :navbar_component, :options, :title
7
7
 
8
8
  def template(&block)
9
- extra_styles = settings.extra_styles
9
+ extra_styles = TinyAdmin.settings.extra_styles
10
10
  flash_component&.messages = messages
11
11
  head_component&.update_attributes(page_title: title, style_links: style_links, extra_styles: extra_styles)
12
12
 
@@ -49,7 +49,7 @@ module TinyAdmin
49
49
  end
50
50
 
51
51
  def style_links
52
- settings.style_links || [
52
+ TinyAdmin.settings.style_links || [
53
53
  # Bootstrap CDN
54
54
  {
55
55
  href: 'https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css',
@@ -61,7 +61,7 @@ module TinyAdmin
61
61
  end
62
62
 
63
63
  def render_scripts
64
- (settings.scripts || []).each do |script_attrs|
64
+ (TinyAdmin.settings.scripts || []).each do |script_attrs|
65
65
  script(**script_attrs)
66
66
  end
67
67
  end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TinyAdmin
4
+ module Views
5
+ module Pages
6
+ class Content < DefaultLayout
7
+ def template
8
+ super do
9
+ div(class: 'content') {
10
+ div(class: 'content-data') {
11
+ unsafe_raw(content)
12
+ }
13
+
14
+ render TinyAdmin::Views::Components::Widgets.new(widgets)
15
+ }
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
21
+ 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
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
 
@@ -12,15 +13,27 @@ loader.setup
12
13
 
13
14
  module TinyAdmin
14
15
  def configure(&block)
15
- block&.call(TinyAdmin::Settings.instance) || TinyAdmin::Settings.instance
16
+ block&.call(settings) || settings
16
17
  end
17
18
 
18
19
  def configure_from_file(file)
20
+ settings.reset!
19
21
  config = YAML.load_file(file, symbolize_names: true)
20
22
  config.each do |key, value|
21
- TinyAdmin::Settings.instance[key] = value
23
+ settings[key] = value
22
24
  end
23
25
  end
24
26
 
25
- module_function :configure, :configure_from_file
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
+
34
+ def settings
35
+ TinyAdmin::Settings.instance
36
+ end
37
+
38
+ module_function :configure, :configure_from_file, :route_for, :settings
26
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.5.0
4
+ version: 0.7.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-24 00:00:00.000000000 Z
11
+ date: 2023-05-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: phlex
@@ -87,20 +87,25 @@ 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
98
101
  - lib/tiny_admin/views/components/filters_form.rb
99
102
  - lib/tiny_admin/views/components/flash.rb
100
103
  - lib/tiny_admin/views/components/head.rb
101
104
  - lib/tiny_admin/views/components/navbar.rb
102
105
  - lib/tiny_admin/views/components/pagination.rb
106
+ - lib/tiny_admin/views/components/widgets.rb
103
107
  - lib/tiny_admin/views/default_layout.rb
108
+ - lib/tiny_admin/views/pages/content.rb
104
109
  - lib/tiny_admin/views/pages/page_not_found.rb
105
110
  - lib/tiny_admin/views/pages/record_not_found.rb
106
111
  - lib/tiny_admin/views/pages/root.rb