tiny_admin 0.5.0 → 0.7.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: 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