activeadmin-command_palette 0.1.6

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 (33) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +51 -0
  3. data/CODE_OF_CONDUCT.md +26 -0
  4. data/CONTRIBUTING.md +23 -0
  5. data/GOVERNANCE.md +25 -0
  6. data/LICENSE.md +21 -0
  7. data/README.md +152 -0
  8. data/SECURITY.md +25 -0
  9. data/activeadmin-command_palette.gemspec +68 -0
  10. data/app/assets/controllers/activeadmin_command_palette/command_palette_controller.js +817 -0
  11. data/app/controllers/active_admin/command_palette/options_controller.rb +61 -0
  12. data/app/views/active_admin/command_palette/_palette.html.erb +45 -0
  13. data/config/locales/activeadmin_command_palette.en.yml +21 -0
  14. data/config/locales/activeadmin_command_palette.fi.yml +21 -0
  15. data/config/locales/activeadmin_command_palette.sv.yml +21 -0
  16. data/config/polyrun_coverage.yml +8 -0
  17. data/config/routes.rb +7 -0
  18. data/lib/activeadmin/command_palette/catalog.rb +184 -0
  19. data/lib/activeadmin/command_palette/compiler.rb +159 -0
  20. data/lib/activeadmin/command_palette/configuration.rb +148 -0
  21. data/lib/activeadmin/command_palette/dsl.rb +13 -0
  22. data/lib/activeadmin/command_palette/engine.rb +67 -0
  23. data/lib/activeadmin/command_palette/finder.rb +103 -0
  24. data/lib/activeadmin/command_palette/helper.rb +136 -0
  25. data/lib/activeadmin/command_palette/integration.rb +17 -0
  26. data/lib/activeadmin/command_palette/resource_config.rb +52 -0
  27. data/lib/activeadmin/command_palette/resource_dsl.rb +47 -0
  28. data/lib/activeadmin/command_palette/resource_methods.rb +11 -0
  29. data/lib/activeadmin/command_palette/url_helpers.rb +22 -0
  30. data/lib/activeadmin/command_palette/version.rb +7 -0
  31. data/lib/activeadmin/command_palette.rb +38 -0
  32. data/sig/activeadmin/command_palette.rbs +5 -0
  33. metadata +392 -0
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveAdmin
4
+ module CommandPalette
5
+ class OptionsController < ::ApplicationController
6
+ before_action :prevent_command_palette_caching
7
+ before_action :authenticate_command_palette_user!
8
+
9
+ def index
10
+ unless authorized_for_palette?
11
+ head :forbidden
12
+ return
13
+ end
14
+
15
+ render json: finder_options
16
+ end
17
+
18
+ private
19
+
20
+ def finder_options
21
+ arguments = {
22
+ query: params[:q],
23
+ helpers: UrlHelpers.new,
24
+ user: current_palette_user
25
+ }
26
+ return Finder.options_for(kinds: params[:kinds], **arguments) if params[:kinds].present?
27
+
28
+ Finder.options(kind: params[:kind], **arguments)
29
+ end
30
+
31
+ def prevent_command_palette_caching
32
+ response.headers["Cache-Control"] = "no-store"
33
+ end
34
+
35
+ def authenticate_command_palette_user!
36
+ method_name = ActiveAdmin.application.authentication_method
37
+ if method_name.present?
38
+ send(method_name)
39
+ elsif current_palette_user.blank?
40
+ head :unauthorized
41
+ end
42
+ end
43
+
44
+ def authorized_for_palette?
45
+ authorizer = ActiveAdmin::CommandPalette.configuration.authorize_with
46
+ if authorizer
47
+ authorizer.call(self)
48
+ else
49
+ current_palette_user.present?
50
+ end
51
+ end
52
+
53
+ def current_palette_user
54
+ method_name = ActiveAdmin.application.current_user_method
55
+ return unless method_name.present?
56
+
57
+ send(method_name)
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,45 @@
1
+ <div
2
+ data-activeadmin-command-palette--command-palette-target="dialog"
3
+ class="hidden fixed inset-0 z-50 flex items-start justify-center pt-[12vh] px-4"
4
+ role="dialog"
5
+ aria-modal="true"
6
+ aria-label="<%= t("active_admin.command_palette.title") %>"
7
+ data-action="keydown->activeadmin-command-palette--command-palette#onDialogKeydown">
8
+ <div
9
+ class="absolute inset-0 bg-black/40"
10
+ data-action="click->activeadmin-command-palette--command-palette#close"
11
+ aria-hidden="true"></div>
12
+ <div
13
+ class="relative w-full max-w-xl rounded-lg border border-gray-200 bg-white shadow-2xl dark:border-gray-700 dark:bg-gray-900"
14
+ data-activeadmin-command-palette--command-palette-target="panel">
15
+ <div class="border-b border-gray-200 px-3 py-2 dark:border-gray-700">
16
+ <input
17
+ type="search"
18
+ autocomplete="off"
19
+ role="combobox"
20
+ aria-expanded="false"
21
+ aria-label="<%= t("active_admin.command_palette.query_label") %>"
22
+ aria-haspopup="listbox"
23
+ aria-controls="activeadmin-command-palette-list"
24
+ aria-autocomplete="list"
25
+ data-activeadmin-command-palette--command-palette-target="query"
26
+ data-action="input->activeadmin-command-palette--command-palette#onInput keydown->activeadmin-command-palette--command-palette#onKeydown"
27
+ placeholder="<%= t("active_admin.command_palette.placeholder") %>"
28
+ class="w-full border-0 bg-transparent text-base text-gray-950 dark:text-gray-100 focus:outline-none focus:ring-0" />
29
+ </div>
30
+ <ul
31
+ id="activeadmin-command-palette-list"
32
+ data-activeadmin-command-palette--command-palette-target="list"
33
+ class="max-h-80 overflow-auto py-1"
34
+ role="listbox"></ul>
35
+ <div
36
+ class="sr-only"
37
+ role="status"
38
+ aria-live="polite"
39
+ aria-atomic="true"
40
+ data-activeadmin-command-palette--command-palette-target="announcer"></div>
41
+ <div class="border-t border-gray-200 px-3 py-2 text-xs text-gray-500 dark:border-gray-700 dark:text-gray-400">
42
+ <%= t("active_admin.command_palette.footer") %>
43
+ </div>
44
+ </div>
45
+ </div>
@@ -0,0 +1,21 @@
1
+ en:
2
+ active_admin:
3
+ command_palette:
4
+ title: Command palette
5
+ open: Open command palette
6
+ open_short: Search
7
+ query_label: Search commands
8
+ placeholder: "Go to… > actions ? help"
9
+ footer: "↑↓ navigate ↵ open esc close"
10
+ empty: No matches
11
+ loading: Searching…
12
+ error: Search failed. Try again.
13
+ unauthorized: Session expired. Sign in again to keep searching.
14
+ forbidden: You do not have permission to search these records.
15
+ required: required
16
+ optional: optional
17
+ help:
18
+ go: "Go / Find — type to jump pages and records"
19
+ actions: "> Actions — run registered commands"
20
+ resource: "> View / Create — search or open new; Edit / Delete on a record page"
21
+ modes: "? Help — show these mode tips"
@@ -0,0 +1,21 @@
1
+ fi:
2
+ active_admin:
3
+ command_palette:
4
+ title: Komentovalikko
5
+ open: Avaa komentovalikko
6
+ open_short: Haku
7
+ query_label: Hae komentoja
8
+ placeholder: "Siirry… > toiminnot ? ohje"
9
+ footer: "↑↓ siirry ↵ avaa esc sulje"
10
+ empty: Ei osumia
11
+ loading: Haetaan…
12
+ error: Haku epäonnistui. Yritä uudelleen.
13
+ unauthorized: Istunto päättyi. Kirjaudu uudelleen jatkaaksesi hakua.
14
+ forbidden: Sinulla ei ole oikeutta hakea näitä tietueita.
15
+ required: pakollinen
16
+ optional: valinnainen
17
+ help:
18
+ go: "Siirry / Haku — kirjoita sivuille ja tietueisiin"
19
+ actions: "> Toiminnot — suorita rekisteröityjä komentoja"
20
+ resource: "> Näytä / Luo — hae tai avaa uusi; Muokkaa / Poista tietuesivulla"
21
+ modes: "? Ohje — näytä nämä tilavihjeet"
@@ -0,0 +1,21 @@
1
+ sv:
2
+ active_admin:
3
+ command_palette:
4
+ title: Kommandopalett
5
+ open: Öppna kommandopaletten
6
+ open_short: Sök
7
+ query_label: Sök kommandon
8
+ placeholder: "Gå till… > åtgärder ? hjälp"
9
+ footer: "↑↓ navigera ↵ öppna esc stäng"
10
+ empty: Inga träffar
11
+ loading: Söker…
12
+ error: Sökningen misslyckades. Försök igen.
13
+ unauthorized: Sessionen har gått ut. Logga in igen för att fortsätta söka.
14
+ forbidden: Du har inte behörighet att söka efter dessa poster.
15
+ required: obligatorisk
16
+ optional: valfri
17
+ help:
18
+ go: "Gå / Sök — skriv för att hoppa till sidor och poster"
19
+ actions: "> Åtgärder — kör registrerade kommandon"
20
+ resource: "> Visa / Skapa — sök eller öppna ny; Redigera / Ta bort på postsida"
21
+ modes: "? Hjälp — visa dessa tipslägen"
@@ -0,0 +1,8 @@
1
+ track_under:
2
+ - lib
3
+ - app
4
+ minimum_line_percent: 50
5
+ report_formats:
6
+ - html
7
+ - cobertura
8
+ - console
data/config/routes.rb ADDED
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ ActiveAdmin::CommandPalette::Engine.routes.draw do
4
+ scope module: "active_admin/command_palette" do
5
+ match "options", to: "options#index", as: :options, via: [:get, :post]
6
+ end
7
+ end
@@ -0,0 +1,184 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveAdmin
4
+ module CommandPalette
5
+ class Catalog
6
+ class << self
7
+ def navigate(view)
8
+ menu = view.try(:current_menu)
9
+ return [] if menu.blank?
10
+
11
+ walk_menu(menu, view)
12
+ rescue NoMethodError, TypeError, ArgumentError
13
+ []
14
+ end
15
+
16
+ def actions(view, context: nil)
17
+ context = normalize_context(context || view.try(:command_palette_context))
18
+
19
+ ActiveAdmin::CommandPalette.configuration.actions.filter_map do |entry|
20
+ next unless matches_context?(entry, context)
21
+ next unless enabled?(entry, view)
22
+
23
+ verb = entry[:verb].to_s
24
+ url = resolve_url(view, entry, context)
25
+ next if url.blank? && verb != "view"
26
+
27
+ item = {
28
+ id: entry[:id],
29
+ kind: "action",
30
+ label: entry[:label],
31
+ url: url,
32
+ method: (entry[:method] || "get").to_s,
33
+ confirm: entry[:confirm],
34
+ scope: entry[:scope],
35
+ when: entry[:when],
36
+ resource: entry[:resource],
37
+ verb: entry[:verb],
38
+ params: entry[:params],
39
+ help: entry[:help],
40
+ finder_kind: entry[:finder_kind]
41
+ }
42
+ detail = entry[:detail].presence || entry[:subtitle].presence
43
+ item[:detail] = detail.to_s if detail.present?
44
+ keywords = Array(entry[:keywords]).map { |keyword| keyword.to_s.presence }.compact
45
+ item[:keywords] = keywords if keywords.any?
46
+ item
47
+ end
48
+ end
49
+
50
+ def resolve_url(view, entry, context = {})
51
+ context = normalize_context(context)
52
+ url =
53
+ if entry[:url].present?
54
+ interpolate(entry[:url], context)
55
+ else
56
+ path = entry[:path]
57
+ return if path.blank?
58
+
59
+ args = path_args_for(entry, context)
60
+ return if entry[:scope].to_s == "member" && !entry.key?(:path_args) && args.empty?
61
+
62
+ view.public_send(path, *args)
63
+ end
64
+
65
+ append_query(url, entry[:query], context)
66
+ rescue NoMethodError, ActionController::UrlGenerationError
67
+ nil
68
+ end
69
+
70
+ def matches_context?(entry, context)
71
+ context = normalize_context(context)
72
+ scope = (entry[:scope] || "global").to_s
73
+ when_list = Array(entry[:when] || "any").map(&:to_s)
74
+ action = gated_action(context[:action])
75
+
76
+ return false if scope == "member" && context[:record_param].blank?
77
+ return false if scope == "collection" && action != "index"
78
+ return false unless when_list.include?("any") || when_list.include?(action) || when_list.include?(context[:action].to_s)
79
+
80
+ if %w[member collection].include?(scope) && entry[:resource].present?
81
+ return false if context[:resource].to_s != entry[:resource].to_s
82
+ end
83
+
84
+ true
85
+ end
86
+
87
+ def enabled?(entry, view)
88
+ return true unless entry.key?(:if)
89
+
90
+ condition = entry[:if]
91
+ return !!condition unless condition.respond_to?(:call)
92
+
93
+ !!view.instance_exec(&condition)
94
+ end
95
+
96
+ private
97
+
98
+ def normalize_context(context)
99
+ hash = (context || {}).transform_keys(&:to_sym)
100
+ {
101
+ namespace: hash[:namespace],
102
+ resource: hash[:resource],
103
+ resource_param: hash[:resource_param],
104
+ record_param: hash[:record_param],
105
+ action: hash[:action].to_s.presence || "other",
106
+ page: hash[:page]
107
+ }
108
+ end
109
+
110
+ def gated_action(action)
111
+ case action.to_s
112
+ when "create" then "new"
113
+ when "update" then "edit"
114
+ else action.to_s
115
+ end
116
+ end
117
+
118
+ def path_args_for(entry, context)
119
+ return Array(entry[:path_args]) if entry.key?(:path_args)
120
+
121
+ if entry[:scope].to_s == "member"
122
+ record = context[:record_param]
123
+ return [] if record.blank?
124
+
125
+ return [record]
126
+ end
127
+
128
+ []
129
+ end
130
+
131
+ def interpolate(value, context)
132
+ value.to_s
133
+ .gsub("%{record_param}", context[:record_param].to_s)
134
+ .gsub("%{resource}", context[:resource].to_s)
135
+ end
136
+
137
+ def append_query(url, query, context)
138
+ return url if url.blank? || query.blank?
139
+
140
+ pairs = query.filter_map do |key, value|
141
+ interpolated = interpolate(value, context)
142
+ next if interpolated.blank?
143
+
144
+ [key.to_s, interpolated]
145
+ end
146
+ return url if pairs.empty?
147
+
148
+ uri, fragment = url.to_s.split("#", 2)
149
+ base, existing = uri.split("?", 2)
150
+ merged = Rack::Utils.parse_nested_query(existing.to_s)
151
+ pairs.each { |key, value| merged[key] = value }
152
+ qs = merged.to_query
153
+ built = qs.present? ? "#{base}?#{qs}" : base
154
+ fragment.present? ? "#{built}##{fragment}" : built
155
+ end
156
+
157
+ def walk_menu(menu, view, parents: [])
158
+ menu.items(view).flat_map do |item|
159
+ label = item.label(view).to_s
160
+ children = item.items(view).presence
161
+ crumbs = parents + [label]
162
+
163
+ if children
164
+ walk_menu(item, view, parents: crumbs)
165
+ elsif (url = item.url(view)).present?
166
+ keywords = crumbs.map { |crumb| crumb.to_s.downcase.strip }.reject(&:blank?).uniq
167
+ entry = {
168
+ id: "nav-#{item.id}",
169
+ kind: "navigate",
170
+ label: crumbs.join(" › "),
171
+ url: url,
172
+ method: "get"
173
+ }
174
+ entry[:keywords] = keywords if keywords.any?
175
+ [entry]
176
+ else
177
+ []
178
+ end
179
+ end
180
+ end
181
+ end
182
+ end
183
+ end
184
+ end
@@ -0,0 +1,159 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveAdmin
4
+ module CommandPalette
5
+ class Compiler
6
+ Registration = Struct.new(
7
+ :kind,
8
+ :noun,
9
+ :resource_name,
10
+ :resource_class,
11
+ :find_columns,
12
+ :label_method,
13
+ :order_column,
14
+ :show_path,
15
+ :new_path,
16
+ :edit_path
17
+ )
18
+
19
+ class << self
20
+ def compile
21
+ actions = []
22
+ finders = []
23
+
24
+ each_resource do |resource|
25
+ palette = resource.command_palette_config
26
+ next unless palette.enabled?
27
+
28
+ registration = build_registration(resource, palette)
29
+ actions.concat(standard_actions_for(registration, palette))
30
+ actions.concat(member_actions_for(registration, palette))
31
+ finders << registration if registration.find_columns.any?
32
+ end
33
+
34
+ {actions: actions, finders: finders}
35
+ end
36
+
37
+ private
38
+
39
+ def each_resource
40
+ return unless defined?(ActiveAdmin) && ActiveAdmin.application
41
+
42
+ ActiveAdmin.application.namespaces.each do |namespace|
43
+ namespace.resources.each do |resource|
44
+ next unless resource.is_a?(ActiveAdmin::Resource)
45
+ next unless resource.respond_to?(:command_palette_config)
46
+
47
+ yield resource
48
+ end
49
+ end
50
+ end
51
+
52
+ def build_registration(resource, palette)
53
+ namespace = resource.namespace.name.to_s
54
+ singular = resource.resource_name.singular
55
+ kind = palette.kind.presence || resource.resource_class.name.demodulize.underscore
56
+ noun = palette.noun.presence || kind.tr("_", " ")
57
+
58
+ Registration.new(
59
+ kind: kind,
60
+ noun: noun,
61
+ resource_name: resource.resource_class.name.demodulize,
62
+ resource_class: resource.resource_class,
63
+ find_columns: palette.find_columns,
64
+ label_method: palette.label_method,
65
+ order_column: palette.order_column,
66
+ show_path: :"#{namespace}_#{singular}_path",
67
+ new_path: :"new_#{namespace}_#{singular}_path",
68
+ edit_path: :"edit_#{namespace}_#{singular}_path"
69
+ )
70
+ end
71
+
72
+ def standard_actions_for(registration, palette)
73
+ actions = []
74
+ noun = registration.noun
75
+ kind = registration.kind
76
+ resource = registration.resource_name
77
+
78
+ if palette.standard_actions.include?(:view)
79
+ actions << {
80
+ id: "view-#{kind.tr("_", "-")}",
81
+ label: "View #{noun}",
82
+ verb: "view",
83
+ finder_kind: kind,
84
+ scope: "global",
85
+ when: "any",
86
+ help: "View #{noun} — type a search term"
87
+ }
88
+ end
89
+
90
+ if palette.standard_actions.include?(:create)
91
+ entry = {
92
+ id: "create-#{kind.tr("_", "-")}",
93
+ label: "Create #{noun}",
94
+ verb: "create",
95
+ path: registration.new_path,
96
+ scope: "global",
97
+ when: "any",
98
+ help: palette.create_help.presence || "Create #{noun}"
99
+ }
100
+ entry[:params] = palette.create_params if palette.create_params.any?
101
+ actions << entry
102
+ end
103
+
104
+ if palette.standard_actions.include?(:edit)
105
+ actions << {
106
+ id: "edit-#{kind.tr("_", "-")}",
107
+ label: "Edit #{noun}",
108
+ verb: "edit",
109
+ path: registration.edit_path,
110
+ scope: "member",
111
+ when: "show",
112
+ resource: resource
113
+ }
114
+ end
115
+
116
+ if palette.standard_actions.include?(:destroy)
117
+ actions << {
118
+ id: "delete-#{kind.tr("_", "-")}",
119
+ label: "Delete #{noun}",
120
+ verb: "destroy",
121
+ path: registration.show_path,
122
+ method: "delete",
123
+ confirm: "Delete this #{noun}?",
124
+ scope: "member",
125
+ when: "show",
126
+ resource: resource
127
+ }
128
+ end
129
+
130
+ actions
131
+ end
132
+
133
+ def member_actions_for(registration, palette)
134
+ palette.members.map do |member|
135
+ id = member[:id].presence || "#{member[:label].to_s.parameterize}-#{registration.kind.tr("_", "-")}"
136
+ entry = {
137
+ id: id,
138
+ label: member[:label],
139
+ scope: member.fetch(:scope, "member"),
140
+ when: member.fetch(:when, "show"),
141
+ resource: registration.resource_name,
142
+ method: member[:method] || "get",
143
+ confirm: member[:confirm],
144
+ path: member[:path],
145
+ url: member[:url],
146
+ path_args: member.key?(:path_args) ? member[:path_args] : nil,
147
+ query: member[:query],
148
+ verb: member[:verb] || "custom",
149
+ if: member.key?(:if) ? member[:if] : nil
150
+ }
151
+ entry.delete(:path_args) if entry[:path_args].nil?
152
+ entry.delete(:if) unless member.key?(:if)
153
+ entry.compact
154
+ end
155
+ end
156
+ end
157
+ end
158
+ end
159
+ end
@@ -0,0 +1,148 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveAdmin
4
+ module CommandPalette
5
+ class Configuration
6
+ DEFAULT_MIN_QUERY_LENGTH = 2
7
+ DEFAULT_LIMIT = 20
8
+ DEFAULT_SEARCH_KIND_LIMIT = 20
9
+ ALLOWED_METHODS = %w[get post put delete].freeze
10
+ ALLOWED_SCOPES = %w[global collection member].freeze
11
+ ALLOWED_WHENS = %w[any index show new edit].freeze
12
+
13
+ attr_accessor :min_query_length, :limit, :search_kind_limit, :authorize_with, :default_finder
14
+ attr_reader :finders, :actions, :prefixes
15
+
16
+ def initialize
17
+ @min_query_length = DEFAULT_MIN_QUERY_LENGTH
18
+ @limit = DEFAULT_LIMIT
19
+ @search_kind_limit = DEFAULT_SEARCH_KIND_LIMIT
20
+ @finders = {}
21
+ @manual_finders = {}
22
+ @actions = []
23
+ @global_actions = []
24
+ @prefixes = {}
25
+ @authorize_with = nil
26
+ @default_finder = nil
27
+ end
28
+
29
+ def register_finder(kind, &block)
30
+ raise ArgumentError, "finder block required" unless block
31
+
32
+ kind = kind.to_s
33
+ @manual_finders[kind] = block
34
+ @finders[kind] = block
35
+ end
36
+
37
+ def register_prefix(prefix, kind:)
38
+ @prefixes[prefix.to_s] = kind.to_s
39
+ end
40
+
41
+ # Global allow-list (non-resource shortcuts). Resource DSL actions merge in on reload!.
42
+ def actions=(entries)
43
+ @global_actions = Array(entries).map { |entry| normalize_action(entry) }
44
+ @actions = @global_actions
45
+ end
46
+
47
+ def finder_kinds
48
+ @finders.keys
49
+ end
50
+
51
+ def reload!
52
+ compiled = Compiler.compile
53
+ @finders = @manual_finders.dup
54
+ compiled[:finders].each do |registration|
55
+ next if @finders.key?(registration.kind)
56
+ next if default_finder.blank?
57
+
58
+ @finders[registration.kind] = finder_proc_for(registration)
59
+ end
60
+ @actions = @global_actions + compiled[:actions].map { |entry| normalize_action(entry) }
61
+ self
62
+ end
63
+
64
+ private
65
+
66
+ def finder_proc_for(registration)
67
+ builder = default_finder
68
+ lambda do |query:, helpers:, user:, limit:|
69
+ builder.call(
70
+ registration: registration,
71
+ query: query,
72
+ helpers: helpers,
73
+ user: user,
74
+ limit: limit
75
+ )
76
+ end
77
+ end
78
+
79
+ def normalize_action(entry)
80
+ hash = entry.transform_keys(&:to_sym)
81
+ raise ArgumentError, "action id required" if hash[:id].to_s.strip.empty?
82
+ raise ArgumentError, "action label required" if hash[:label].to_s.strip.empty?
83
+
84
+ method = (hash[:method] || "get").to_s.downcase
85
+ unless ALLOWED_METHODS.include?(method)
86
+ raise ArgumentError, "unsupported action method #{method.inspect}"
87
+ end
88
+
89
+ if %w[post put delete].include?(method) && hash[:confirm].to_s.strip.empty?
90
+ raise ArgumentError, "POST, PUT, and DELETE actions require non-empty confirm text"
91
+ end
92
+
93
+ scope = (hash[:scope] || "global").to_s
94
+ unless ALLOWED_SCOPES.include?(scope)
95
+ raise ArgumentError, "unsupported action scope #{scope.inspect}"
96
+ end
97
+
98
+ when_list = normalize_when(hash[:when])
99
+ params = Array(hash[:params]).map { |param| normalize_param(param) }
100
+ query = normalize_query(hash[:query])
101
+
102
+ normalized = hash.merge(
103
+ method: method,
104
+ scope: scope,
105
+ when: when_list,
106
+ params: params,
107
+ query: query
108
+ )
109
+ normalized[:verb] = hash[:verb].to_s if hash[:verb].present?
110
+ normalized[:resource] = hash[:resource].to_s if hash[:resource].present?
111
+ normalized[:help] = hash[:help].to_s if hash[:help].present?
112
+ normalized[:finder_kind] = hash[:finder_kind].to_s if hash[:finder_kind].present?
113
+ normalized[:path_args] = Array(hash[:path_args]) if hash.key?(:path_args)
114
+ normalized[:if] = hash[:if] if hash.key?(:if)
115
+ normalized
116
+ end
117
+
118
+ def normalize_when(value)
119
+ list = Array(value.nil? ? "any" : value).flat_map { |item| item.to_s.split(",") }.map(&:strip)
120
+ list = ["any"] if list.empty?
121
+ list.each do |item|
122
+ unless ALLOWED_WHENS.include?(item)
123
+ raise ArgumentError, "unsupported action when #{item.inspect}"
124
+ end
125
+ end
126
+ list
127
+ end
128
+
129
+ def normalize_param(entry)
130
+ hash = entry.transform_keys(&:to_sym)
131
+ raise ArgumentError, "param name required" if hash[:name].to_s.strip.empty?
132
+
133
+ {
134
+ name: hash[:name].to_s,
135
+ required: hash[:required] ? true : false,
136
+ help: hash[:help]&.to_s,
137
+ query_key: hash[:query_key]&.to_s
138
+ }.compact
139
+ end
140
+
141
+ def normalize_query(value)
142
+ return {} if value.blank?
143
+
144
+ value.to_h.transform_keys(&:to_s).transform_values(&:to_s)
145
+ end
146
+ end
147
+ end
148
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveAdmin
4
+ module CommandPalette
5
+ module DSL
6
+ def command_palette(&block)
7
+ definition = ResourceDSL.new(config.command_palette_config)
8
+ definition.instance_exec(&block) if block
9
+ config.command_palette_config
10
+ end
11
+ end
12
+ end
13
+ end