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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +51 -0
- data/CODE_OF_CONDUCT.md +26 -0
- data/CONTRIBUTING.md +23 -0
- data/GOVERNANCE.md +25 -0
- data/LICENSE.md +21 -0
- data/README.md +152 -0
- data/SECURITY.md +25 -0
- data/activeadmin-command_palette.gemspec +68 -0
- data/app/assets/controllers/activeadmin_command_palette/command_palette_controller.js +817 -0
- data/app/controllers/active_admin/command_palette/options_controller.rb +61 -0
- data/app/views/active_admin/command_palette/_palette.html.erb +45 -0
- data/config/locales/activeadmin_command_palette.en.yml +21 -0
- data/config/locales/activeadmin_command_palette.fi.yml +21 -0
- data/config/locales/activeadmin_command_palette.sv.yml +21 -0
- data/config/polyrun_coverage.yml +8 -0
- data/config/routes.rb +7 -0
- data/lib/activeadmin/command_palette/catalog.rb +184 -0
- data/lib/activeadmin/command_palette/compiler.rb +159 -0
- data/lib/activeadmin/command_palette/configuration.rb +148 -0
- data/lib/activeadmin/command_palette/dsl.rb +13 -0
- data/lib/activeadmin/command_palette/engine.rb +67 -0
- data/lib/activeadmin/command_palette/finder.rb +103 -0
- data/lib/activeadmin/command_palette/helper.rb +136 -0
- data/lib/activeadmin/command_palette/integration.rb +17 -0
- data/lib/activeadmin/command_palette/resource_config.rb +52 -0
- data/lib/activeadmin/command_palette/resource_dsl.rb +47 -0
- data/lib/activeadmin/command_palette/resource_methods.rb +11 -0
- data/lib/activeadmin/command_palette/url_helpers.rb +22 -0
- data/lib/activeadmin/command_palette/version.rb +7 -0
- data/lib/activeadmin/command_palette.rb +38 -0
- data/sig/activeadmin/command_palette.rbs +5 -0
- 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"
|
data/config/routes.rb
ADDED
|
@@ -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
|