admin_suite 0.2.2 → 0.2.3

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: 22d3fb1dd5649136ae88ea54aa4b948790c1e1fd7a81681ffd07707f82d6494a
4
- data.tar.gz: 00c3ba3433eb3d9d59289c70d75fe9b0a3f613a043b6dc1d109b7af4a375272e
3
+ metadata.gz: ed1253c0c2d8c5d658db9ad2bc569e0f0b2c76d8ed4741f3069d0e2240619848
4
+ data.tar.gz: dc3dd2e0686581694f2d71f77b440bfe56606c867d4a7103e3d1148aecf5b3ed
5
5
  SHA512:
6
- metadata.gz: b104f41152647c491a4f318beb6d1b2ea7f69619af6c7cb9ee05fef3a32b719aef73f48200e6b0bd5efc4c2ad7fe85637db40dc3023d63a3281cd39882539b7f
7
- data.tar.gz: 8d27c97e8790b50d59f769abacdc1a19158158fcb284d31b6e3785fcb8064e3020ef8577e3b1960681038314ea865e1502ba408e7db8359924ea5accc39bb99e
6
+ metadata.gz: fb266215e70e3aa1aea7ff9e4ed49801c3960b7ef7c82c966f7e4417f0d7b3b84c88ca4a27b4fe2c1fdbc584932b78ce3bf7e621f885ec8ceacb3a48e7bb5d90
7
+ data.tar.gz: 426c328efa276ac354a5776ab4636643747a2d8d4fc6849f789742ed03d3573b2fa14fbcc4451b5ba25c4f88ff95a546f3410c2d770b79e9d61dfc7195e14ec3
@@ -508,6 +508,7 @@ button[type="submit"]:hover,
508
508
  transition: background-color 0.2s ease-in-out;
509
509
  background: #e2e8f0; /* slate-200 (off) */
510
510
  vertical-align: middle;
511
+ overflow: hidden;
511
512
  }
512
513
 
513
514
  .admin-suite-toggle-wrap {
@@ -96,6 +96,7 @@ module AdminSuite
96
96
  # POST /:portal/:resource_name/:id/toggle
97
97
  def toggle
98
98
  field = params[:field].presence&.to_sym
99
+ field ||= toggleable_fields.first if toggleable_fields.one?
99
100
  unless field
100
101
  head :unprocessable_entity
101
102
  return
@@ -112,7 +113,7 @@ module AdminSuite
112
113
  respond_to do |format|
113
114
  format.turbo_stream do
114
115
  render turbo_stream: turbo_stream.replace(
115
- dom_id(@resource, :toggle),
116
+ dom_id(@resource, "#{field}_toggle"),
116
117
  partial: "admin_suite/shared/toggle_cell",
117
118
  locals: { record: @resource, field: field, toggle_url: resource_toggle_path(portal: current_portal, resource_name: resource_name, id: @resource.to_param, field: field) }
118
119
  )
@@ -218,13 +219,22 @@ module AdminSuite
218
219
  end
219
220
 
220
221
  def toggleable_fields
221
- return [] unless resource_config&.index_config&.columns_list
222
+ return [] unless resource_config
222
223
 
223
- resource_config.index_config.columns_list.filter_map do |col|
224
+ index_fields = resource_config.index_config&.columns_list&.filter_map do |col|
224
225
  next unless col.type == :toggle
225
226
 
226
227
  (col.toggle_field || col.name).to_sym
227
- end
228
+ end || []
229
+
230
+ form_fields = resource_config.form_config&.fields_list&.filter_map do |field|
231
+ next unless field.is_a?(Admin::Base::Resource::FieldDefinition)
232
+ next unless field.type == :toggle
233
+
234
+ field.name.to_sym
235
+ end || []
236
+
237
+ (index_fields + form_fields).uniq
228
238
  end
229
239
 
230
240
  def resource_url(record)
@@ -28,6 +28,45 @@ module AdminSuite
28
28
  end
29
29
  end
30
30
 
31
+ # Logout path/method/label in the topbar are host-configurable.
32
+ def admin_suite_logout_path
33
+ value = AdminSuite.config.respond_to?(:logout_path) ? AdminSuite.config.logout_path : nil
34
+ resolve_admin_suite_view_config(value).presence
35
+ end
36
+
37
+ def admin_suite_logout_method
38
+ value = AdminSuite.config.respond_to?(:logout_method) ? AdminSuite.config.logout_method : :delete
39
+ resolved = resolve_admin_suite_view_config(value)
40
+ resolved = resolved.to_sym if resolved.respond_to?(:to_sym)
41
+ resolved.presence || :delete
42
+ rescue StandardError
43
+ :delete
44
+ end
45
+
46
+ def admin_suite_logout_label
47
+ value = AdminSuite.config.respond_to?(:logout_label) ? AdminSuite.config.logout_label : nil
48
+ resolved = resolve_admin_suite_view_config(value)
49
+ resolved.to_s.presence || "Log out"
50
+ end
51
+
52
+ def resolve_admin_suite_view_config(value)
53
+ return nil if value.nil?
54
+
55
+ if value.respond_to?(:call)
56
+ return value.call if value.arity.zero?
57
+ return value.call(self)
58
+ end
59
+
60
+ if value.is_a?(Symbol)
61
+ return nil unless respond_to?(value, true)
62
+ return public_send(value)
63
+ end
64
+
65
+ value
66
+ rescue StandardError
67
+ nil
68
+ end
69
+
31
70
  # Lookup the DSL field definition for a given attribute (if present).
32
71
  #
33
72
  # Used to render show values with type awareness (e.g. markdown/json/label).
@@ -1062,7 +1101,7 @@ module AdminSuite
1062
1101
  action: "input->admin-suite--searchable-select#search focus->admin-suite--searchable-select#open keydown->admin-suite--searchable-select#keydown"
1063
1102
  }))
1064
1103
  concat(content_tag(:div, "",
1065
- class: "absolute z-10 w-full mt-1 bg-white border border-slate-200 rounded-lg shadow-lg hidden max-h-60 overflow-y-auto",
1104
+ class: "absolute z-40 w-full mt-1 bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-lg shadow-lg hidden max-h-60 overflow-y-auto",
1066
1105
  data: { "admin-suite--searchable-select-target": "dropdown" }))
1067
1106
  end
1068
1107
  end
@@ -0,0 +1,45 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ // Connects to data-controller="admin-suite--flash"
4
+ export default class extends Controller {
5
+ static targets = ["notification"]
6
+
7
+ connect() {
8
+ this.timeout = setTimeout(() => this.dismissAll(), 5000)
9
+ }
10
+
11
+ dismiss(event) {
12
+ if (event) {
13
+ event.preventDefault()
14
+ }
15
+
16
+ const notification = event?.currentTarget?.closest('[data-admin-suite--flash-target="notification"]')
17
+ if (notification) {
18
+ this.fadeOut(notification)
19
+ }
20
+ }
21
+
22
+ dismissAll() {
23
+ this.notificationTargets.forEach((notification) => this.fadeOut(notification))
24
+ }
25
+
26
+ fadeOut(element) {
27
+ if (!element || !element.isConnected) return
28
+
29
+ element.style.transition = "opacity 0.25s ease-out, transform 0.25s ease-out"
30
+ element.style.opacity = "0"
31
+ element.style.transform = "translateX(8px)"
32
+
33
+ setTimeout(() => {
34
+ if (element.isConnected) {
35
+ element.remove()
36
+ }
37
+ }, 250)
38
+ }
39
+
40
+ disconnect() {
41
+ if (this.timeout) {
42
+ clearTimeout(this.timeout)
43
+ }
44
+ }
45
+ }
@@ -11,7 +11,8 @@ export default class extends Controller {
11
11
  }
12
12
 
13
13
  connect() {
14
- this.checked = this.inputTarget?.value === "1" || this.inputTarget?.value === "true"
14
+ const inputValue = this.hasInputTarget ? this.inputTarget.value : "0"
15
+ this.checked = inputValue === "1" || inputValue === "true"
15
16
  this.updateVisual()
16
17
  }
17
18
 
@@ -31,27 +32,27 @@ export default class extends Controller {
31
32
  }
32
33
 
33
34
  updateVisual() {
35
+ const inactiveTokens = this.classTokens(this.inactiveClasses)
36
+
34
37
  if (this.hasButtonTarget) {
35
38
  if (this.checked) {
36
- this.buttonTarget.classList.remove(...this.inactiveClasses.split(" "))
37
- this.buttonTarget.classList.add(this.activeClass)
39
+ if (inactiveTokens.length > 0) {
40
+ this.buttonTarget.classList.remove(...inactiveTokens)
41
+ }
42
+ if (this.activeClass) {
43
+ this.buttonTarget.classList.add(this.activeClass)
44
+ }
38
45
  } else {
39
- this.buttonTarget.classList.remove(this.activeClass)
40
- this.buttonTarget.classList.add(...this.inactiveClasses.split(" "))
46
+ if (this.activeClass) {
47
+ this.buttonTarget.classList.remove(this.activeClass)
48
+ }
49
+ if (inactiveTokens.length > 0) {
50
+ this.buttonTarget.classList.add(...inactiveTokens)
51
+ }
41
52
  }
42
53
  this.buttonTarget.setAttribute("aria-checked", this.checked.toString())
43
54
  }
44
55
 
45
- if (this.hasThumbTarget) {
46
- if (this.checked) {
47
- this.thumbTarget.classList.remove("translate-x-0")
48
- this.thumbTarget.classList.add("translate-x-5")
49
- } else {
50
- this.thumbTarget.classList.remove("translate-x-5")
51
- this.thumbTarget.classList.add("translate-x-0")
52
- }
53
- }
54
-
55
56
  if (this.hasLabelTarget) {
56
57
  this.labelTarget.textContent = this.checked ? "Enabled" : "Disabled"
57
58
  }
@@ -62,5 +63,13 @@ export default class extends Controller {
62
63
  this.inputTarget.value = this.checked ? "1" : "0"
63
64
  }
64
65
  }
66
+
67
+ classTokens(value) {
68
+ return value
69
+ .toString()
70
+ .split(/\s+/)
71
+ .map((token) => token.trim())
72
+ .filter((token) => token.length > 0)
73
+ }
65
74
  }
66
75
 
@@ -16,7 +16,7 @@
16
16
  </div>
17
17
 
18
18
  <!-- Form -->
19
- <div class="bg-white rounded-xl border border-slate-200 overflow-hidden">
19
+ <div class="bg-white dark:bg-slate-800 rounded-xl border border-slate-200 dark:border-slate-700 overflow-visible">
20
20
  <div class="p-6">
21
21
  <%= render "admin_suite/shared/form", resource: @resource %>
22
22
  </div>
@@ -14,7 +14,7 @@
14
14
  </div>
15
15
 
16
16
  <!-- Form -->
17
- <div class="bg-white rounded-xl border border-slate-200 overflow-hidden">
17
+ <div class="bg-white dark:bg-slate-800 rounded-xl border border-slate-200 dark:border-slate-700 overflow-visible">
18
18
  <div class="p-6">
19
19
  <%= render "admin_suite/shared/form", resource: @resource %>
20
20
  </div>
@@ -1,5 +1,5 @@
1
1
  <% if flash.any? %>
2
- <div class="fixed top-4 right-4 z-50 space-y-2 max-w-md">
2
+ <div class="fixed top-20 right-4 z-50 space-y-2 max-w-md" data-controller="admin-suite--flash">
3
3
  <% flash.each do |type, message| %>
4
4
  <% type = type.to_sym %>
5
5
  <% classes = case type
@@ -13,7 +13,10 @@
13
13
  "admin-suite-flash--info"
14
14
  end %>
15
15
 
16
- <div class="flex items-start gap-3 px-5 py-4 rounded-lg shadow-lg border <%= classes %>">
16
+ <div
17
+ data-admin-suite--flash-target="notification"
18
+ class="flex items-start gap-3 px-5 py-4 rounded-lg shadow-lg border <%= classes %>"
19
+ >
17
20
  <div class="flex-shrink-0 mt-0.5">
18
21
  <% icon_name = case type
19
22
  when :notice then "check-circle-2"
@@ -23,7 +26,17 @@
23
26
  end %>
24
27
  <%= admin_suite_icon(icon_name, class: "w-5 h-5") %>
25
28
  </div>
29
+
26
30
  <p class="flex-1 text-sm font-medium leading-snug"><%= message %></p>
31
+
32
+ <button
33
+ type="button"
34
+ class="flex-shrink-0 text-slate-400 hover:text-slate-600 transition-colors"
35
+ data-action="admin-suite--flash#dismiss"
36
+ aria-label="Dismiss notification"
37
+ >
38
+ <%= admin_suite_icon("x", class: "w-4 h-4") %>
39
+ </button>
27
40
  </div>
28
41
  <% end %>
29
42
  </div>
@@ -7,6 +7,7 @@
7
7
  %>
8
8
  <%
9
9
  value = record.public_send(field)
10
+ frame_id = dom_id(record, "#{field}_toggle")
10
11
  toggle_url ||= begin
11
12
  url_for(action: :toggle, id: record.id, field: field)
12
13
  rescue ActionController::UrlGenerationError
@@ -16,11 +17,12 @@
16
17
  end
17
18
  %>
18
19
 
19
- <%= turbo_frame_tag dom_id(record, :toggle), class: "admin-suite-toggle-wrap inline-flex align-middle" do %>
20
+ <%= turbo_frame_tag frame_id, class: "admin-suite-toggle-wrap inline-flex align-middle" do %>
20
21
  <% if toggle_url %>
21
22
  <%= button_to toggle_url,
22
23
  method: :post,
23
- form: { data: { turbo_frame: dom_id(record, :toggle) }, class: "m-0 inline-flex align-middle items-center" },
24
+ params: { field: field },
25
+ form: { data: { turbo_frame: frame_id }, class: "m-0 inline-flex align-middle items-center" },
24
26
  class: "admin-suite-toggle-track #{value ? 'is-on' : ''}" do %>
25
27
  <span class="sr-only">Toggle <%= field.to_s.humanize %></span>
26
28
  <span class="admin-suite-toggle-thumb"></span>
@@ -42,6 +42,19 @@
42
42
  <%= (admin_suite_actor&.respond_to?(:name) && admin_suite_actor.name.present?) ? admin_suite_actor.name.first.upcase : "A" %>
43
43
  </div>
44
44
  </div>
45
+
46
+ <% logout_path = admin_suite_logout_path %>
47
+ <% if logout_path.present? %>
48
+ <div class="pl-3 border-l border-slate-200">
49
+ <%= button_to logout_path,
50
+ method: admin_suite_logout_method,
51
+ form: { class: "inline-flex" },
52
+ class: "inline-flex items-center gap-1.5 px-3 py-2 rounded-lg text-sm font-medium text-slate-600 hover:text-red-600 hover:bg-red-50 transition-colors" do %>
53
+ <%= admin_suite_icon("log-out", class: "w-4 h-4") %>
54
+ <span class="hidden sm:inline"><%= admin_suite_logout_label %></span>
55
+ <% end %>
56
+ </div>
57
+ <% end %>
45
58
  </div>
46
59
  </div>
47
60
  </header>
@@ -29,6 +29,9 @@ These are the defaults in `AdminSuite::Configuration` / `AdminSuite::Engine`:
29
29
  - `authenticate`: `nil`
30
30
  - `current_actor`: `nil`
31
31
  - `authorize`: `nil`
32
+ - `logout_path`: `nil`
33
+ - `logout_method`: `:delete`
34
+ - `logout_label`: `"Log out"`
32
35
  - `resource_globs`: defaults to:
33
36
  - `Rails.root/config/admin_suite/resources/*.rb`
34
37
  - `Rails.root/app/admin/resources/*.rb`
@@ -97,6 +100,33 @@ Optional authorization hook (you can wire Pundit/CanCan/ActionPolicy/etc).
97
100
 
98
101
  Note: this hook is available, but your app must call it from resource definitions / custom actions as needed (AdminSuite will not guess your authorization policy).
99
102
 
103
+ ### `logout_path`
104
+
105
+ Optional sign-out action shown in the top bar.
106
+
107
+ - **Type**: `Proc`, `String`, `Symbol`, or `nil`
108
+ - **Proc signature**: `->(view_context) { ... }`
109
+
110
+ Example:
111
+
112
+ ```ruby
113
+ config.logout_path = ->(view) { view.main_app.internal_developer_logout_path }
114
+ ```
115
+
116
+ ### `logout_method`
117
+
118
+ HTTP method for the topbar sign-out button.
119
+
120
+ - **Type**: `Symbol` or `String`
121
+ - **Default**: `:delete`
122
+
123
+ ### `logout_label`
124
+
125
+ Button label for the topbar sign-out action.
126
+
127
+ - **Type**: `String` (or `Proc` for dynamic label)
128
+ - **Default**: `"Log out"`
129
+
100
130
  ### `resource_globs`
101
131
 
102
132
  Where AdminSuite should load resource definition files from.
@@ -6,6 +6,9 @@ module AdminSuite
6
6
  attr_accessor :authenticate,
7
7
  :current_actor,
8
8
  :authorize,
9
+ :logout_path,
10
+ :logout_method,
11
+ :logout_label,
9
12
  :resource_globs,
10
13
  :action_globs,
11
14
  :portal_globs,
@@ -30,6 +33,9 @@ module AdminSuite
30
33
  @authenticate = nil
31
34
  @current_actor = nil
32
35
  @authorize = nil
36
+ @logout_path = nil
37
+ @logout_method = :delete
38
+ @logout_label = "Log out"
33
39
  @resource_globs = []
34
40
  @action_globs = []
35
41
  @portal_globs = []
@@ -12,6 +12,8 @@ module AdminSuite
12
12
 
13
13
  if (field_def = admin_suite_field_definition(field_name))
14
14
  case field_def.type
15
+ when :toggle
16
+ return render_show_toggle(record, field_def.name)
15
17
  when :markdown
16
18
  rendered =
17
19
  if defined?(::MarkdownRenderer)
@@ -40,8 +42,13 @@ module AdminSuite
40
42
  # If the field isn't in the form config, fall back to index column config
41
43
  # so show pages can still render labels consistently.
42
44
  if respond_to?(:resource_config, true) && (rc = resource_config) && rc.index_config&.columns_list
43
- col = rc.index_config.columns_list.find { |c| c.name.to_sym == field_name.to_sym }
44
- if col&.type == :label
45
+ col = rc.index_config.columns_list.find do |c|
46
+ c.name.to_sym == field_name.to_sym || c.toggle_field&.to_sym == field_name.to_sym
47
+ end
48
+ if col&.type == :toggle
49
+ toggle_field = (col.toggle_field || col.name).to_sym
50
+ return render_show_toggle(record, toggle_field)
51
+ elsif col&.type == :label
45
52
  label_value = col.content.is_a?(Proc) ? col.content.call(record) : value
46
53
  return render_label_badge(label_value, color: col.label_color, size: col.label_size, record: record)
47
54
  end
@@ -65,6 +72,27 @@ module AdminSuite
65
72
 
66
73
  super
67
74
  end
75
+
76
+ private
77
+
78
+ def render_show_toggle(record, field)
79
+ toggle_url =
80
+ begin
81
+ resource_toggle_path(
82
+ portal: current_portal,
83
+ resource_name: resource_name,
84
+ id: record.to_param,
85
+ field: field
86
+ )
87
+ rescue StandardError
88
+ nil
89
+ end
90
+
91
+ render(
92
+ partial: "admin_suite/shared/toggle_cell",
93
+ locals: { record: record, field: field, toggle_url: toggle_url }
94
+ )
95
+ end
68
96
  end
69
97
  end
70
98
  end
@@ -2,7 +2,7 @@
2
2
 
3
3
  module AdminSuite
4
4
  module Version
5
- VERSION = "0.2.2"
5
+ VERSION = "0.2.3"
6
6
  end
7
7
 
8
8
  # Backward-compatible constant.
@@ -14,6 +14,11 @@ AdminSuite.configure do |config|
14
14
  # config.authorize = ->(actor, action:, subject:, resource:, controller:) { true }
15
15
  config.authorize = nil
16
16
 
17
+ # Optional sign-out action in the topbar.
18
+ # config.logout_path = ->(view) { view.main_app.logout_path }
19
+ # config.logout_method = :delete
20
+ # config.logout_label = "Log out"
21
+
17
22
  # Resource definition file globs (host app can override).
18
23
  config.resource_globs = [
19
24
  Rails.root.join("config/admin_suite/resources/*.rb").to_s,
metadata CHANGED
@@ -1,13 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: admin_suite
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.2
4
+ version: 0.2.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - TechWright Labs
8
+ autorequire:
8
9
  bindir: bin
9
10
  cert_chain: []
10
- date: 1980-01-02 00:00:00.000000000 Z
11
+ date: 2026-02-14 00:00:00.000000000 Z
11
12
  dependencies:
12
13
  - !ruby/object:Gem::Dependency
13
14
  name: rails
@@ -139,6 +140,7 @@ files:
139
140
  - app/javascript/controllers/admin_suite/clipboard_controller.js
140
141
  - app/javascript/controllers/admin_suite/code_editor_controller.js
141
142
  - app/javascript/controllers/admin_suite/file_upload_controller.js
143
+ - app/javascript/controllers/admin_suite/flash_controller.js
142
144
  - app/javascript/controllers/admin_suite/json_editor_controller.js
143
145
  - app/javascript/controllers/admin_suite/live_filter_controller.js
144
146
  - app/javascript/controllers/admin_suite/markdown_editor_controller.js
@@ -237,7 +239,6 @@ files:
237
239
  - test/dummy/config/puma.rb
238
240
  - test/dummy/config/routes.rb
239
241
  - test/dummy/db/seeds.rb
240
- - test/dummy/log/test.log
241
242
  - test/dummy/public/400.html
242
243
  - test/dummy/public/404.html
243
244
  - test/dummy/public/406-unsupported-browser.html
@@ -247,7 +248,6 @@ files:
247
248
  - test/dummy/public/icon.svg
248
249
  - test/dummy/public/robots.txt
249
250
  - test/dummy/test/test_helper.rb
250
- - test/dummy/tmp/local_secret.txt
251
251
  - test/fixtures/docs/progress/PROGRESS_REPORT.md
252
252
  - test/integration/dashboard_test.rb
253
253
  - test/integration/docs_test.rb
@@ -264,6 +264,7 @@ metadata:
264
264
  rubygems_mfa_required: 'true'
265
265
  source_code_uri: https://github.com/techwright-lab/admin_suite
266
266
  changelog_uri: https://github.com/techwright-lab/admin_suite/blob/main/CHANGELOG.md
267
+ post_install_message:
267
268
  rdoc_options: []
268
269
  require_paths:
269
270
  - lib
@@ -278,7 +279,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
278
279
  - !ruby/object:Gem::Version
279
280
  version: '0'
280
281
  requirements: []
281
- rubygems_version: 3.6.9
282
+ rubygems_version: 3.5.22
283
+ signing_key:
282
284
  specification_version: 4
283
285
  summary: Reusable admin suite engine
284
286
  test_files: []