madmin 2.0.4 → 2.1.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.
Files changed (34) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/stylesheets/madmin/application.css +0 -1
  3. data/app/assets/stylesheets/madmin/base.css +32 -5
  4. data/app/assets/stylesheets/madmin/forms.css +32 -6
  5. data/app/assets/stylesheets/madmin/pagination.css +1 -1
  6. data/app/assets/stylesheets/madmin/sidebar.css +99 -2
  7. data/app/assets/stylesheets/madmin/tables.css +14 -9
  8. data/app/controllers/madmin/base_controller.rb +6 -1
  9. data/app/controllers/madmin/resource_controller.rb +0 -3
  10. data/app/helpers/madmin/application_helper.rb +1 -1
  11. data/app/javascript/madmin/controllers/mobile_nav_controller.js +50 -0
  12. data/app/javascript/madmin/controllers/select_controller.js +8 -3
  13. data/app/views/layouts/madmin/application.html.erb +22 -6
  14. data/app/views/madmin/application/_missing_resource.html.erb +8 -0
  15. data/app/views/madmin/application/_navigation.html.erb +0 -2
  16. data/app/views/madmin/application/edit.html.erb +1 -1
  17. data/app/views/madmin/application/index.html.erb +10 -3
  18. data/app/views/madmin/dashboard/show.html.erb +2 -4
  19. data/app/views/madmin/fields/belongs_to/_show.html.erb +5 -1
  20. data/app/views/madmin/fields/has_many/_show.html.erb +12 -5
  21. data/app/views/madmin/fields/has_one/_show.html.erb +5 -1
  22. data/app/views/madmin/fields/nested_has_many/_fields.html.erb +3 -2
  23. data/app/views/madmin/fields/nested_has_many/_show.html.erb +12 -5
  24. data/app/views/madmin/fields/polymorphic/_show.html.erb +5 -1
  25. data/app/views/madmin/fields/time/_form.html.erb +4 -3
  26. data/lib/madmin/engine.rb +1 -1
  27. data/lib/madmin/fields/belongs_to.rb +8 -2
  28. data/lib/madmin/fields/has_many.rb +35 -9
  29. data/lib/madmin/fields/has_one.rb +4 -0
  30. data/lib/madmin/fields/polymorphic.rb +5 -0
  31. data/lib/madmin/resource.rb +1 -1
  32. data/lib/madmin/version.rb +1 -1
  33. data/lib/madmin.rb +24 -5
  34. metadata +4 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5738233eebbda8b8b68dd7a00e0bb4006d77a80cea0b75b1466b5f67dfe67e5c
4
- data.tar.gz: d28a9f663c5fbd612a1b1ad7593414ad3ee8125d3d194d9c1efbc4d2f88b2a49
3
+ metadata.gz: de0ee3515b8f8689bbd5c7b6b99675c2724f0be4cf7774e97d71da99076f3b68
4
+ data.tar.gz: 4224940c11670cfbc2a481cc0001ad64d846595929993f5971490c1f53e0343c
5
5
  SHA512:
6
- metadata.gz: 91f19c83273374d2eafc04873fec3e06575ecd2525745ae3237aa13485ef1d682872cc21996602c67f7b7ee8d3c01f54bcdf8163124360ca0c6020200ab8d962
7
- data.tar.gz: 2cf083d97e766b6e8a43662caa217dd6dbbf8df913a6339f6ca88db57bf579cfb7b32ff443ff7d4dc185f73f0708c56f3766b0d02508963d446882fd8445e865
6
+ metadata.gz: eaf9f28c5e3dac557dceaff92f823b3372d9c927a2c429d296da83bee28c3f14cd78b31bf198bde7d43bfc8025aaa01d96e1586edc42ebef5c49e3f827929cfc
7
+ data.tar.gz: e0b561bb2c7d0367238ba4c9af7794161480396ccf2bc625ffad2a3adfbeb1b6919490f59a8fc24c1eac89eec4f0162e202186dc47913f41351b893972c2fb13
@@ -7,4 +7,3 @@
7
7
  @import url("/madmin/forms.css");
8
8
  @import url("/madmin/tables.css");
9
9
  @import url("/madmin/pagination.css");
10
-
@@ -10,17 +10,26 @@
10
10
  body {
11
11
  color: var(--text-color);
12
12
  font-size: 14px;
13
+ -webkit-font-smoothing: antialiased;
14
+ -moz-osx-font-smoothing: grayscale;
15
+
16
+ display: grid;
17
+ min-height: 100dvh;
18
+ grid-template-rows: auto auto 1fr auto;
13
19
  }
14
20
 
15
21
  a {
16
22
  color: var(--primary-color);
17
23
  }
18
24
 
25
+ .size-5 {
26
+ width: 1.25rem;
27
+ height: 1.25rem;
28
+ }
29
+
19
30
  .alert {
20
- border-radius: 0.5rem;
21
31
  font-weight: 500;
22
32
  padding: 1rem;
23
- margin-bottom: 1rem;
24
33
 
25
34
  ul {
26
35
  margin-top: 0.5rem;
@@ -61,12 +70,13 @@ a {
61
70
  }
62
71
 
63
72
  .header {
64
- border-bottom: 1px solid rgb(229 231 235);
65
73
  display: flex;
74
+ flex-wrap: wrap;
66
75
  justify-content: space-between;
67
76
  align-items: center;
77
+ padding-top: 1rem;
68
78
  padding-bottom: 1rem;
69
- margin-bottom: 1rem;
79
+ gap: 0.5rem;
70
80
 
71
81
  h1 {
72
82
  margin: 0;
@@ -83,12 +93,15 @@ a {
83
93
  .actions {
84
94
  align-items: center;
85
95
  display: flex;
96
+ flex-wrap: wrap;
86
97
  gap: 0.5rem;
87
98
  }
88
99
  }
89
100
 
90
101
  .metrics {
91
- display: flex;
102
+ display: grid;
103
+ grid-template-columns: repeat(2, minmax(0, 1fr));
104
+
92
105
 
93
106
  .metric {
94
107
  border: 1px solid rgb(229 231 235);
@@ -115,3 +128,17 @@ a {
115
128
  .scopes {
116
129
  margin-bottom: 1rem;
117
130
  }
131
+
132
+ .hidden {
133
+ display: none;
134
+ }
135
+
136
+ @media (min-width: 768px) {
137
+ .md\:inline-block {
138
+ display: inline-block;
139
+ }
140
+
141
+ .md\:hidden {
142
+ display: none;
143
+ }
144
+ }
@@ -1,10 +1,10 @@
1
+ .error {
2
+ color: rgb(239 68 68);
3
+ }
4
+
1
5
  .form-hint {
2
6
  font-size: 0.875rem;
3
7
  margin-top: 0.5rem;
4
-
5
- &.error {
6
- color: rgb(239 68 68);
7
- }
8
8
  }
9
9
 
10
10
  .form-group {
@@ -26,7 +26,11 @@ label {
26
26
  font-size: 0.875rem;
27
27
  }
28
28
 
29
- button, input, optgroup, select, textarea {
29
+ button,
30
+ input,
31
+ optgroup,
32
+ select,
33
+ textarea {
30
34
  font-family: inherit;
31
35
  font-feature-settings: inherit;
32
36
  font-variation-settings: inherit;
@@ -39,7 +43,22 @@ button, input, optgroup, select, textarea {
39
43
  padding: 0;
40
44
  }
41
45
 
42
- [type='text'], input:where(:not([type])), [type='email'], [type='url'], [type='password'], [type='number'], [type='date'], [type='datetime-local'], [type='month'], [type='search'], [type='tel'], [type='time'], [type='week'], [multiple], textarea, select {
46
+ [type='text'],
47
+ input:where(:not([type])),
48
+ [type='email'],
49
+ [type='url'],
50
+ [type='password'],
51
+ [type='number'],
52
+ [type='date'],
53
+ [type='datetime-local'],
54
+ [type='month'],
55
+ [type='search'],
56
+ [type='tel'],
57
+ [type='time'],
58
+ [type='week'],
59
+ [multiple],
60
+ textarea,
61
+ select {
43
62
  -webkit-appearance: none;
44
63
  -moz-appearance: none;
45
64
  appearance: none;
@@ -62,3 +81,10 @@ button, input, optgroup, select, textarea {
62
81
  color: rgb(239 68 68);
63
82
  font-weight: 600;
64
83
  }
84
+
85
+ .nested-fields {
86
+ border: 1px solid #e5e7eb;
87
+ border-radius: 0.5rem;
88
+ padding: 1.25rem;
89
+ margin-bottom: 1rem;
90
+ }
@@ -35,7 +35,7 @@
35
35
  cursor: default;
36
36
  }
37
37
 
38
- a.current {
38
+ a[aria-current="page"] {
39
39
  color: #ffffff;
40
40
  background-color: var(--primary-color);
41
41
  }
@@ -1,17 +1,60 @@
1
+ #navbar {
2
+ border-bottom: 1px solid var(--border-color);
3
+
4
+ padding-top: 0.5rem;
5
+ padding-bottom: 0.5rem;
6
+ padding-left: 0.5rem;
7
+ padding-right: 0.5rem;
8
+
9
+ display: flex;
10
+ align-items: center;
11
+ justify-content: space-between;
12
+
13
+ .nav-group {
14
+ display: flex;
15
+ gap: 0.5em;
16
+ align-items: center;
17
+ }
18
+
19
+ a {
20
+ border-radius: .375rem;
21
+ color: var(--text-color);
22
+ display: block;
23
+ font-weight: 500;
24
+ padding: 0.5rem;
25
+ text-decoration: none;
26
+
27
+ margin-top: 0.1rem;
28
+ margin-bottom: 0.1rem;
29
+
30
+ &:hover {
31
+ background-color: rgb(243 244 246);
32
+ }
33
+
34
+ &.active {
35
+ background-color: rgb(243 244 246);
36
+ font-weight: 600;
37
+ }
38
+ }
39
+ }
40
+
1
41
  main {
2
- padding-top: 1rem;
3
42
  padding-right: 1rem;
4
43
  padding-bottom: 1rem;
5
44
  padding-left: calc(1rem + var(--sidebar-width));
45
+ position: relative;
6
46
  }
7
47
 
8
48
  #sidebar {
49
+ position: absolute;
50
+ inset: 0;
51
+ z-index: 10; /* Ensure sidebar is above main content */
52
+
9
53
  border-right: 1px solid var(--border-color);
10
54
  height: 100%;
11
55
  margin: 0;
12
56
  overflow: auto;
13
57
  padding: 1rem;
14
- position: fixed;
15
58
  width: var(--sidebar-width);
16
59
 
17
60
  h1 {
@@ -78,3 +121,57 @@ main {
78
121
  }
79
122
  }
80
123
  }
124
+
125
+ /* Mobile Navigation Styles */
126
+ #hamburger-menu {
127
+ display: none;
128
+ background: none;
129
+ border: none;
130
+ padding: 0.5rem;
131
+ cursor: pointer;
132
+ color: var(--text-color);
133
+ }
134
+
135
+ .mobile-nav-overlay {
136
+ opacity: 0;
137
+ position: fixed;
138
+ top: 0;
139
+ left: 0;
140
+ width: 100%;
141
+ height: 100%;
142
+ background: rgba(0, 0, 0, 0.15);
143
+ z-index: 40;
144
+ transition: 0.2s ease-in-out;
145
+ pointer-events: none;
146
+ }
147
+
148
+ .mobile-nav-overlay.is-active {
149
+ opacity: 1;
150
+ pointer-events: auto;
151
+ }
152
+
153
+ @media (max-width: 768px) {
154
+ #hamburger-menu {
155
+ display: block;
156
+ }
157
+
158
+ main {
159
+ /* reset padding */
160
+ padding-left: 1rem;
161
+ }
162
+
163
+ #sidebar {
164
+ position: fixed;
165
+ top: 0;
166
+ left: 0;
167
+ height: 100vh;
168
+ transform: translateX(-100%);
169
+ transition: transform 0.2s ease-in-out;
170
+ background-color: var(--background-color, #fff);
171
+ z-index: 50;
172
+ }
173
+
174
+ #sidebar.open {
175
+ transform: translateX(0);
176
+ }
177
+ }
@@ -3,6 +3,8 @@
3
3
  border: 1px solid var(--border-color);
4
4
  overflow-x: auto;
5
5
  position: relative;
6
+ min-width: 0;
7
+ width: 100%;
6
8
  }
7
9
 
8
10
  table {
@@ -13,10 +15,6 @@ table {
13
15
  background-color: var(--background-color);
14
16
  border-bottom: 1px solid var(--border-color);
15
17
  text-align: left;
16
- padding-bottom: 0.75rem;
17
- padding-top: 0.75rem;
18
- padding-right: 0.875rem;
19
- padding-left: 0.875rem;
20
18
 
21
19
  a {
22
20
  color: rgb(17 24 39);
@@ -36,11 +34,6 @@ table {
36
34
  }
37
35
 
38
36
  td {
39
- padding-bottom: 0.75rem;
40
- padding-top: 0.75rem;
41
- padding-right: 0.875rem;
42
- padding-left: 0.875rem;
43
-
44
37
  a {
45
38
  font-weight: 500;
46
39
  }
@@ -53,4 +46,16 @@ table {
53
46
  border-bottom: none;
54
47
  }
55
48
  }
49
+
50
+ th, td {
51
+ /* force scroll */
52
+ min-width: 60px;
53
+ white-space: nowrap;
54
+
55
+ /* responsive padding */
56
+ padding-top: clamp(0.5rem, 0.25rem + 1.25vw, 0.75rem);
57
+ padding-bottom: clamp(0.5rem, 0.25rem + 1.25vw, 0.75rem);
58
+ padding-left: clamp(0.5rem, 0.125rem + 1.875vw, 0.875rem);
59
+ padding-right: clamp(0.5rem, 0.125rem + 1.875vw, 0.875rem);
60
+ }
56
61
  }
@@ -1,7 +1,12 @@
1
1
  module Madmin
2
2
  class BaseController < ActionController::Base
3
3
  include ::ActiveStorage::SetCurrent if defined?(::ActiveStorage)
4
- include Pagy::Backend
4
+
5
+ if Gem::Version.new(Pagy::VERSION) >= Gem::Version.new("43.0.0.rc")
6
+ include Pagy::Method
7
+ else
8
+ include Pagy::Backend
9
+ end
5
10
 
6
11
  protect_from_forgery with: :exception
7
12
  end
@@ -16,9 +16,6 @@ module Madmin
16
16
  render json: @records.map { |r| {name: @resource.display_name(r), id: r.id} }
17
17
  }
18
18
  end
19
- rescue Pagy::OverflowError
20
- params[:page] = 1
21
- retry
22
19
  end
23
20
 
24
21
  def show
@@ -1,6 +1,6 @@
1
1
  module Madmin
2
2
  module ApplicationHelper
3
- include Pagy::Frontend
3
+ include Pagy::Frontend if defined? Pagy::Frontend
4
4
  include Rails.application.routes.url_helpers
5
5
 
6
6
  def clear_search_params
@@ -0,0 +1,50 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ export default class extends Controller {
4
+ static targets = ["menu", "overlay"]
5
+
6
+ connect() {
7
+ this.handleEscape = this.handleEscape.bind(this)
8
+ }
9
+
10
+ toggle() {
11
+ const open = this.menuTarget.classList.toggle("open")
12
+ this.element.setAttribute("aria-expanded", open)
13
+ this.toggleOverlay(open)
14
+ this.toggleBodyScroll(open)
15
+ this.toggleEscapeListener(open)
16
+ }
17
+
18
+ close() {
19
+ if (this.menuTarget.classList.contains("open")) {
20
+ this.toggle()
21
+ }
22
+ }
23
+
24
+ handleEscape(event) {
25
+ if (event.key === "Escape") {
26
+ this.close()
27
+ }
28
+ }
29
+
30
+ disconnect() {
31
+ this.toggleEscapeListener(false)
32
+ this.toggleBodyScroll(false)
33
+ this.close()
34
+ }
35
+
36
+ toggleOverlay(open) {
37
+ if (this.hasOverlayTarget) {
38
+ this.overlayTarget.classList.toggle("is-active", open)
39
+ }
40
+ }
41
+
42
+ toggleBodyScroll(open) {
43
+ document.body.style.overflow = open ? 'hidden' : ''
44
+ }
45
+
46
+ toggleEscapeListener(open) {
47
+ const method = open ? 'addEventListener' : 'removeEventListener'
48
+ document[method]("keydown", this.handleEscape)
49
+ }
50
+ }
@@ -8,12 +8,15 @@ export default class extends Controller {
8
8
  }
9
9
 
10
10
  connect() {
11
- this.select = new TomSelect(this.element, {
11
+ let options = {
12
12
  plugins: ['remove_button'],
13
13
  valueField: 'id',
14
14
  labelField: 'name',
15
15
  searchField: 'name',
16
- load: (search, callback) => {
16
+ }
17
+
18
+ if (this.hasUrlValue) {
19
+ options["load"] = (search, callback) => {
17
20
  let url = search ? `${this.urlValue}?q=${search}` : this.urlValue;
18
21
  fetch(url)
19
22
  .then(response => response.json())
@@ -23,7 +26,9 @@ export default class extends Controller {
23
26
  callback();
24
27
  });
25
28
  }
26
- })
29
+ }
30
+
31
+ this.select = new TomSelect(this.element, options)
27
32
  }
28
33
 
29
34
  disconnect() {
@@ -13,13 +13,29 @@
13
13
  <%= csrf_meta_tags %>
14
14
  <%= render "javascript" %>
15
15
  </head>
16
- <body>
17
- <aside id="sidebar">
18
- <%= render "navigation" %>
19
- </aside>
20
- <main>
16
+ <body class="<%= controller_name %> <%= action_name %>" data-controller="mobile-nav">
17
+ <header>
21
18
  <%= render "flash" %>
22
- <%= yield %>
19
+ </header>
20
+ <header id="navbar">
21
+ <div class="nav-group">
22
+ <button id="hamburger-menu" type="button" data-action="click->mobile-nav#toggle" aria-label="Open menu" aria-expanded="false" aria-controls="sidebar">
23
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="size-5"> <path fill-rule="evenodd" d="M2 4.75A.75.75 0 0 1 2.75 4h14.5a.75.75 0 0 1 0 1.5H2.75A.75.75 0 0 1 2 4.75ZM2 10a.75.75 0 0 1 .75-.75h14.5a.75.75 0 0 1 0 1.5H2.75A.75.75 0 0 1 2 10Zm0 5.25a.75.75 0 0 1 .75-.75h14.5a.75.75 0 0 1 0 1.5H2.75a.75.75 0 0 1-.75-.75Z" clip-rule="evenodd" /> </svg>
24
+ </button>
25
+ <%= link_to_if respond_to?(:root_url), Madmin.site_name, root_url, data: {turbo: false} %>
26
+ </div>
27
+ </header>
28
+
29
+ <main>
30
+ <aside id="sidebar" data-mobile-nav-target="menu">
31
+ <%= render "navigation" %>
32
+ </aside>
33
+
34
+ <div>
35
+ <%= yield %>
36
+ </div.>
23
37
  </main>
38
+
39
+ <div data-mobile-nav-target="overlay" data-action="click->mobile-nav#close" class="mobile-nav-overlay"></div>
24
40
  </body>
25
41
  </html>
@@ -0,0 +1,8 @@
1
+ <div class="error"><%= resource_name %> is missing.</div>
2
+ <% if Rails.env.development? %>
3
+ <div>Create it by running:
4
+ <code>
5
+ bin/rails generate madmin:resource <%= resource_name.split("Resource").first %>
6
+ </code>
7
+ </div>
8
+ <% end %>
@@ -1,5 +1,3 @@
1
- <h1><%= link_to_if respond_to?(:root_url), Madmin.site_name, root_url, data: {turbo: false} %></h1>
2
-
3
1
  <nav>
4
2
  <%= nav_link_to "Dashboard", madmin_root_path %>
5
3
 
@@ -1,4 +1,4 @@
1
- <%= content_for :title, "New #{resource.display_name(@record)}" %>
1
+ <%= content_for :title, "Edit #{resource.display_name(@record)}" %>
2
2
 
3
3
  <header class="header">
4
4
  <h1>
@@ -15,7 +15,9 @@
15
15
  </svg>
16
16
  <% end if params[:q].present? %>
17
17
 
18
- <%= link_to "New #{resource.friendly_name}", resource.new_path, class: "btn btn-secondary" %>
18
+ <%= link_to resource.new_path, class: "btn btn-secondary" do %>
19
+ New <%= tag.span resource.friendly_name, class: "hidden md:inline-block" %>
20
+ <% end %>
19
21
  </div>
20
22
  </header>
21
23
 
@@ -63,6 +65,11 @@
63
65
  </div>
64
66
 
65
67
  <div class="pagination">
66
- <%== pagy_nav @pagy if @pagy.pages > 1 %>
67
- <span>Showing <%= tag.strong @pagy.in %> of <%= tag.strong @pagy.count %></span>
68
+ <% if @pagy.respond_to?(:series_nav) %>
69
+ <%== @pagy.series_nav if @pagy.last > 1 %>
70
+ <%== @pagy.info_tag %>
71
+ <% else %>
72
+ <%== pagy_nav @pagy if @pagy.last > 1 %>
73
+ <%== pagy_info @pagy %>
74
+ <% end %>
68
75
  </div>
@@ -1,4 +1,2 @@
1
- <div class="prose">
2
- <h1>Madmin Dashboard</h1>
3
- <p>Create <code>app/views/madmin/dashboard/show.html.erb</code> to customize your dashboard.</p>
4
- </div>
1
+ <h1>Madmin Dashboard</h1>
2
+ <p>Create <code>app/views/madmin/dashboard/show.html.erb</code> to customize your dashboard.</p>
@@ -1,3 +1,7 @@
1
1
  <% if (object = field.value(record)) %>
2
- <%= link_to Madmin.resource_for(object).display_name(object), Madmin.resource_for(object).show_path(object), class: "text-blue-500 underline" %>
2
+ <% if (associated_resource = field.associated_resource_for(object)) %>
3
+ <%= link_to associated_resource.display_name(object), associated_resource.show_path(object) %>
4
+ <% else %>
5
+ <%= render partial: "missing_resource", locals: { resource_name: Madmin.resource_name_for(object) } %>
6
+ <% end %>
3
7
  <% end %>
@@ -1,11 +1,18 @@
1
1
  <% pagy, records = field.paginated_value(record, params) %>
2
2
  <% records.each do |object| %>
3
- <div>
4
- <%= link_to Madmin.resource_for(object).display_name(object), Madmin.resource_for(object).show_path(object), class: "text-blue-500 underline" %>
5
- </div>
3
+ <% if (associated_resource = field.associated_resource_for(object)) %>
4
+ <div><%= link_to associated_resource.display_name(object), associated_resource.show_path(object) %></div>
5
+ <% else %>
6
+ <%= render partial: "missing_resource", locals: { resource_name: Madmin.resource_name_for(object) } %>
7
+ <% end %>
6
8
  <% end %>
7
9
 
8
10
  <div class="pagination">
9
- <%== pagy_nav pagy if pagy.pages > 1 %>
10
- <span>Showing <%= tag.strong pagy.in %> of <%= tag.strong pagy.count %></span>
11
+ <% if pagy.respond_to?(:series_nav) %>
12
+ <%== pagy.series_nav if pagy.last > 1 %>
13
+ <%== pagy.info_tag %>
14
+ <% else %>
15
+ <%== pagy_nav pagy if pagy.last > 1 %>
16
+ <%== pagy_info pagy %>
17
+ <% end %>
11
18
  </div>
@@ -1,3 +1,7 @@
1
1
  <% if (object = field.value(record)) %>
2
- <%= link_to Madmin.resource_for(object).display_name(object), Madmin.resource_for(object).show_path(object) %>
2
+ <% if (associated_resource = field.associated_resource_for(object)) %>
3
+ <%= link_to associated_resource.display_name(object), associated_resource.show_path(object) %>
4
+ <% else %>
5
+ <%= render partial: "missing_resource", locals: { resource_name: Madmin.resource_name_for(object) } %>
6
+ <% end %>
3
7
  <% end %>
@@ -1,11 +1,12 @@
1
- <%= content_tag :div, class: "nested-fields border border-gray-200 rounded-lg p-5", data: { new_record: f.object.new_record? } do %>
1
+ <%= content_tag :div, class: "nested-fields", data: { new_record: f.object.new_record? } do %>
2
2
  <% field.nested_attributes.each do |name, nested_attribute| %>
3
3
  <% nested_field = nested_attribute.field %>
4
4
  <% next if nested_field.nil? %>
5
5
  <% next unless nested_field.visible?(action_name) %>
6
6
  <% next unless nested_field.visible?(:form) %>
7
7
 
8
- <div class="mb-4 flex">
8
+ <div class="form-group">
9
+ <%= render "madmin/shared/label", form: f, field: nested_field %>
9
10
  <%= render partial: nested_field.to_partial_path("form"), locals: { field: nested_field, record: f.object, form: f, resource: field.resource } %>
10
11
  </div>
11
12
  <% end %>
@@ -1,11 +1,18 @@
1
1
  <% pagy, records = field.paginated_value(record, params) %>
2
2
  <% records.each do |object| %>
3
- <div>
4
- <%= link_to Madmin.resource_for(object).display_name(object), Madmin.resource_for(object).show_path(object), class: "text-blue-500 underline" %>
5
- </div>
3
+ <% if (associated_resource = field.associated_resource_for(object)) %>
4
+ <div><%= link_to associated_resource.display_name(object), associated_resource.show_path(object) %></div>
5
+ <% else %>
6
+ <%= render partial: "missing_resource", locals: { resource_name: Madmin.resource_name_for(object) } %>
7
+ <% end %>
6
8
  <% end %>
7
9
 
8
10
  <div class="pagination">
9
- <%== pagy_nav pagy if pagy.pages > 1 %>
10
- <span>Showing <%= tag.strong pagy.in %> of <%= tag.strong pagy.count %></span>
11
+ <% if pagy.respond_to?(:series_nav) %>
12
+ <%== pagy.series_nav if pagy.last > 1 %>
13
+ <%== pagy.info_tag %>
14
+ <% else %>
15
+ <%== pagy_nav pagy if pagy.last > 1 %>
16
+ <%== pagy_info pagy %>
17
+ <% end %>
11
18
  </div>
@@ -1,3 +1,7 @@
1
1
  <% if (object = field.value(record)) %>
2
- <%= link_to Madmin.resource_for(object).display_name(object), Madmin.resource_for(object).show_path(object), class: "text-blue-500 underline" %>
2
+ <% if (associated_resource = field.associated_resource_for(object)) %>
3
+ <%= link_to associated_resource.display_name(object), associated_resource.show_path(object), class: "text-blue-500 underline" %>
4
+ <% else %>
5
+ <%= render partial: "missing_resource", locals: { resource_name: Madmin.resource_name_for(object) } %>
6
+ <% end %>
3
7
  <% end %>
@@ -1,4 +1,5 @@
1
- <div class="block md:inline-block md:w-32 flex-shrink-0 text-gray-700">
2
- <%= render "madmin/shared/label", form: form, field: field %>
1
+ <div>
2
+ <%= form.time_select field.attribute_name,
3
+ {},
4
+ class: "form-select inline-block" %>
3
5
  </div>
4
- <%= form.time_select field.attribute_name, {}, { class: "form-select" } %>
data/lib/madmin/engine.rb CHANGED
@@ -11,7 +11,7 @@ module Madmin
11
11
 
12
12
  config.to_prepare do
13
13
  Madmin.reset_resources!
14
- Madmin.site_name ||= Rails.application.class.module_parent_name
14
+ Madmin.site_name ||= Rails.application.class.module_parent_name.titleize
15
15
  end
16
16
 
17
17
  initializer "madmin.assets" do |app|
@@ -15,12 +15,18 @@ module Madmin
15
15
  "#{attribute_name}_id"
16
16
  end
17
17
 
18
- def index_path
19
- associated_resource.index_path(format: :json)
18
+ def index_path(format: :json)
19
+ associated_resource&.index_path(format: format)
20
20
  end
21
21
 
22
22
  def associated_resource
23
23
  Madmin.resource_by_name(model.reflect_on_association(attribute_name).klass)
24
+ rescue MissingResource
25
+ end
26
+
27
+ def associated_resource_for(object)
28
+ Madmin.resource_for(object)
29
+ rescue MissingResource
24
30
  end
25
31
  end
26
32
  end
@@ -1,8 +1,6 @@
1
1
  module Madmin
2
2
  module Fields
3
3
  class HasMany < Field
4
- include Pagy::Backend
5
-
6
4
  def options_for_select(record)
7
5
  if (records = record.send(attribute_name))
8
6
  return [] unless records.first
@@ -17,19 +15,47 @@ module Madmin
17
15
  {"#{attribute_name.to_s.singularize}_ids": []}
18
16
  end
19
17
 
20
- def index_path
21
- Madmin.resource_by_name(model.reflect_on_association(attribute_name).klass).index_path(format: :json)
18
+ def index_path(format: :json)
19
+ associated_resource&.index_path(format: format)
20
+ end
21
+
22
+ def associated_resource
23
+ Madmin.resource_by_name(model.reflect_on_association(attribute_name).klass)
24
+ rescue MissingResource
25
+ end
26
+
27
+ def associated_resource_for(object)
28
+ Madmin.resource_for(object)
29
+ rescue MissingResource
22
30
  end
23
31
 
24
32
  def paginateable?
25
33
  true
26
34
  end
27
35
 
28
- def paginated_value(record, params)
29
- param_name = "#{attribute_name}_page"
30
- pagy value(record), page: params[param_name].to_i, page_param: param_name
31
- rescue Pagy::OverflowError, Pagy::VariableError
32
- pagy value(record), page: 1, page_param: param_name
36
+ if Gem::Version.new(Pagy::VERSION) >= Gem::Version.new("43.0.0.rc")
37
+ include Pagy::Method
38
+
39
+ def paginated_value(record, params)
40
+ page_key = "#{attribute_name}_page"
41
+ request = {
42
+ query: {
43
+ "#{attribute_name}_page" => [params[page_key].to_i, 1].max
44
+ }
45
+ }
46
+ pagy value(record), page_key: page_key, request: request
47
+ rescue Pagy::OptionError
48
+ end
49
+ else
50
+ include Pagy::Backend
51
+
52
+ def paginated_value(record, params)
53
+ page_key = "#{attribute_name}_page"
54
+ page = [params[page_key].to_i, 1].max
55
+ pagy value(record), page: page, page_param: page_key
56
+ rescue Pagy::OverflowError, Pagy::VariableError
57
+ pagy value, page: 1, page_param: page_key
58
+ end
33
59
  end
34
60
  end
35
61
  end
@@ -1,6 +1,10 @@
1
1
  module Madmin
2
2
  module Fields
3
3
  class HasOne < Field
4
+ def associated_resource_for(object)
5
+ Madmin.resource_for(object)
6
+ rescue MissingResource
7
+ end
4
8
  end
5
9
  end
6
10
  end
@@ -12,6 +12,11 @@ module Madmin
12
12
  def to_param
13
13
  {attribute_name => %i[type value]}
14
14
  end
15
+
16
+ def associated_resource_for(object)
17
+ Madmin.resource_for(object)
18
+ rescue MissingResource
19
+ end
15
20
  end
16
21
  end
17
22
  end
@@ -72,7 +72,7 @@ module Madmin
72
72
  end
73
73
 
74
74
  def friendly_name
75
- model_name.gsub("::", " / ").split(/(?=[A-Z])/).join(" ")
75
+ model_name.split("::").map { |part| part.underscore.humanize }.join(" / ").titlecase.pluralize
76
76
  end
77
77
 
78
78
  # Support for isolated namespaces
@@ -1,3 +1,3 @@
1
1
  module Madmin
2
- VERSION = "2.0.4"
2
+ VERSION = "2.1.0"
3
3
  end
data/lib/madmin.rb CHANGED
@@ -41,19 +41,32 @@ module Madmin
41
41
  mattr_accessor :site_name
42
42
  mattr_accessor :stylesheets, default: []
43
43
 
44
+ class MissingResource < StandardError
45
+ end
46
+
44
47
  class << self
45
48
  def resource_for(object)
49
+ if (resource_name = resource_name_for(object))
50
+ resource_name.constantize
51
+ end
52
+ rescue NameError
53
+ raise MissingResource, <<~MESSAGE
54
+ #{resource_name} is missing. Create it by running:
55
+
56
+ bin/rails generate madmin:resource #{resource_name.split("Resource").first}
57
+ MESSAGE
58
+ end
59
+
60
+ def resource_name_for(object)
46
61
  if object.is_a? ::ActiveStorage::Attached
47
- "ActiveStorage::AttachmentResource".constantize
62
+ "ActiveStorage::AttachmentResource"
48
63
  else
49
64
  begin
50
- "#{object.class.name}Resource".constantize
65
+ "#{object.class.name}Resource"
51
66
  rescue
52
67
  # For STI models, see if there's a superclass resource available
53
68
  if (column = object.class.inheritance_column) && object.class.column_names.include?(column)
54
- "#{object.class.superclass.base_class.name}Resource".constantize
55
- else
56
- raise
69
+ "#{object.class.superclass.base_class.name}Resource"
57
70
  end
58
71
  end
59
72
  end
@@ -61,6 +74,12 @@ module Madmin
61
74
 
62
75
  def resource_by_name(name)
63
76
  "#{name}Resource".constantize
77
+ rescue NameError
78
+ raise MissingResource, <<~MESSAGE
79
+ #{name}Resource is missing. Create it by running:
80
+
81
+ bin/rails generate madmin:resource #{resource_name.split("Resource").first}
82
+ MESSAGE
64
83
  end
65
84
 
66
85
  def resources
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: madmin
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.4
4
+ version: 2.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Chris Oliver
@@ -98,12 +98,14 @@ files:
98
98
  - app/javascript/madmin/application.js
99
99
  - app/javascript/madmin/controllers/application.js
100
100
  - app/javascript/madmin/controllers/index.js
101
+ - app/javascript/madmin/controllers/mobile_nav_controller.js
101
102
  - app/javascript/madmin/controllers/nested_form_controller.js
102
103
  - app/javascript/madmin/controllers/select_controller.js
103
104
  - app/views/layouts/madmin/application.html.erb
104
105
  - app/views/madmin/application/_flash.html.erb
105
106
  - app/views/madmin/application/_form.html.erb
106
107
  - app/views/madmin/application/_javascript.html.erb
108
+ - app/views/madmin/application/_missing_resource.html.erb
107
109
  - app/views/madmin/application/_navigation.html.erb
108
110
  - app/views/madmin/application/edit.html.erb
109
111
  - app/views/madmin/application/index.html.erb
@@ -255,7 +257,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
255
257
  - !ruby/object:Gem::Version
256
258
  version: '0'
257
259
  requirements: []
258
- rubygems_version: 3.6.9
260
+ rubygems_version: 3.7.1
259
261
  specification_version: 4
260
262
  summary: A modern admin for Ruby on Rails apps
261
263
  test_files: []