kommandant 0.1.0 → 0.2.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/app/assets/builds/kommandant.css +634 -0
- data/app/assets/stylesheets/kommandant/application.tailwind.css +3 -0
- data/app/controllers/concerns/kommandant/recent_commands.rb +21 -0
- data/app/controllers/kommandant/application_controller.rb +6 -1
- data/app/controllers/kommandant/commands/searches_controller.rb +40 -0
- data/app/controllers/kommandant/commands_controller.rb +17 -0
- data/app/controllers/kommandant/searches_controller.rb +14 -0
- data/app/models/kommandant/command.rb +88 -0
- data/app/models/kommandant/commands/search_result.rb +44 -0
- data/app/views/kommandant/commands/searches/show.html.erb +69 -0
- data/app/views/kommandant/commands/show.html.erb +20 -0
- data/app/views/kommandant/searches/index.html.erb +1 -0
- data/app/views/kommandant/searches/new.html.erb +1 -0
- data/app/views/kommandant/shared/_command_palette.html.erb +53 -0
- data/app/views/kommandant/shared/command_palette/_command.html.erb +14 -0
- data/app/views/kommandant/shared/command_palette/_default_state.html.erb +14 -0
- data/app/views/kommandant/shared/command_palette/_empty_state.html.erb +7 -0
- data/app/views/kommandant/shared/command_palette/_loading_message.html.erb +7 -0
- data/app/views/kommandant/shared/command_palette/_result.html.erb +6 -0
- data/app/views/kommandant/shared/icons/_chevron_right.html.erb +3 -0
- data/app/views/kommandant/shared/icons/_command.erb +7 -0
- data/app/views/kommandant/shared/icons/_search.erb +3 -0
- data/app/views/kommandant/shared/icons/_spinner.erb +4 -0
- data/config/locales/da.yml +19 -0
- data/config/locales/en.yml +19 -0
- data/config/routes.rb +4 -0
- data/lib/kommandant/engine.rb +7 -0
- data/lib/kommandant/version.rb +1 -1
- data/lib/kommandant.rb +21 -1
- data/vendor/assets/javascripts/command_palette.js +120 -0
- data/vendor/assets/javascripts/keyboard_navigation.js +72 -0
- data/vendor/assets/javascripts/kommandant.js +2 -0
- data/vendor/assets/javascripts/transition.js +57 -0
- metadata +93 -3
@@ -0,0 +1,17 @@
|
|
1
|
+
module Kommandant
|
2
|
+
class CommandsController < ApplicationController
|
3
|
+
before_action :set_command
|
4
|
+
|
5
|
+
def show
|
6
|
+
if Kommandant.config.recent_commands.enabled
|
7
|
+
current_user.recent_commands.prepend(@command.id)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
private
|
12
|
+
|
13
|
+
def set_command
|
14
|
+
@command = Kommandant::Command.find(params[:id])
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module Kommandant
|
2
|
+
class SearchesController < ApplicationController
|
3
|
+
def index
|
4
|
+
@results = Kommandant::Command.search(params[:query])
|
5
|
+
|
6
|
+
unless Kommandant.config.admin_only_filter_lambda.present? && Kommandant.config.admin_only_filter_lambda.call(current_user, current_admin)
|
7
|
+
@results.reject!(&:admin_only?)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
def new
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
module Kommandant
|
2
|
+
class Command
|
3
|
+
class << self
|
4
|
+
def reindex!
|
5
|
+
index.add_documents(
|
6
|
+
JSON.parse(File.read(Kommandant.config.commands_path))
|
7
|
+
)
|
8
|
+
end
|
9
|
+
|
10
|
+
def search(query)
|
11
|
+
result = index.search(query)["hits"]
|
12
|
+
|
13
|
+
result.map do |command|
|
14
|
+
new(**command.symbolize_keys)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def find(id)
|
19
|
+
search(id).find { |command| command.id == id }
|
20
|
+
end
|
21
|
+
|
22
|
+
def all
|
23
|
+
JSON.parse(File.read(Kommandant.config.commands_path)).map do |hash|
|
24
|
+
Kommandant::Command.new(**hash.symbolize_keys)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def index
|
29
|
+
client.index("commands")
|
30
|
+
end
|
31
|
+
|
32
|
+
def client
|
33
|
+
MeiliSearch::Rails.client
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def initialize(id:, icon:, path:, http_method:, resource_class: nil, redirect_path: nil, text_keys: [], admin_only: false, translations: {})
|
38
|
+
@id = id
|
39
|
+
@icon = icon
|
40
|
+
@path = path
|
41
|
+
@http_method = http_method
|
42
|
+
@resource_class = resource_class
|
43
|
+
@redirect_path = redirect_path
|
44
|
+
@text_keys = text_keys
|
45
|
+
@admin_only = admin_only
|
46
|
+
@translator = I18n::Backend::KeyValue.new({})
|
47
|
+
translations.each_pair do |locale, data|
|
48
|
+
@translator.store_translations(locale, data)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def ==(other)
|
53
|
+
instance_variables.all? do |ivar|
|
54
|
+
instance_variable_get(ivar) == other.instance_variable_get(ivar)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
attr_reader :id, :path, :http_method, :resource_class, :redirect_path, :text_keys, :translator
|
59
|
+
|
60
|
+
def name
|
61
|
+
return "no translation for #{I18n.locale}.name in Meilisearch" unless translator.exists?(I18n.locale, "name")
|
62
|
+
|
63
|
+
translator.translate(I18n.locale, "name")
|
64
|
+
end
|
65
|
+
|
66
|
+
def placeholder
|
67
|
+
return unless translator.exists?(I18n.locale, "placeholder")
|
68
|
+
|
69
|
+
translator.translate(I18n.locale, "placeholder")
|
70
|
+
end
|
71
|
+
|
72
|
+
def admin_only?
|
73
|
+
admin_only
|
74
|
+
end
|
75
|
+
|
76
|
+
def icon?
|
77
|
+
icon.present?
|
78
|
+
end
|
79
|
+
|
80
|
+
def icon
|
81
|
+
@icon.to_s.dasherize
|
82
|
+
end
|
83
|
+
|
84
|
+
private
|
85
|
+
|
86
|
+
attr_reader :admin_only
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module Kommandant
|
2
|
+
class Commands::SearchResult
|
3
|
+
def initialize(command:, resource:)
|
4
|
+
@command = command
|
5
|
+
@resource = resource
|
6
|
+
@translator = command.translator
|
7
|
+
end
|
8
|
+
|
9
|
+
attr_reader :resource
|
10
|
+
delegate :icon, to: :command
|
11
|
+
|
12
|
+
def name
|
13
|
+
if translator.exists?(I18n.locale, "result_text")
|
14
|
+
translation_attributes = {}
|
15
|
+
command.text_keys.each do |key|
|
16
|
+
translation_attributes[key.to_sym] = indexed_attribute_for(resource, key)
|
17
|
+
end
|
18
|
+
translator.translate(I18n.locale, "result_text", **translation_attributes)
|
19
|
+
else
|
20
|
+
command.text_keys.map do |text_key|
|
21
|
+
indexed_attribute_for(resource, text_key)
|
22
|
+
end.join(", ")
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def path
|
27
|
+
return command.redirect_path.gsub(":id", resource.id.to_s) if command.redirect_path
|
28
|
+
|
29
|
+
"/#{command.resource_class.downcase.pluralize}/#{resource.id}"
|
30
|
+
end
|
31
|
+
|
32
|
+
def http_method
|
33
|
+
:get
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
attr_reader :command, :translator
|
39
|
+
|
40
|
+
def indexed_attribute_for(resource, text_key)
|
41
|
+
resource.meilisearch_settings.get_attributes(resource)[text_key]
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
<%= turbo_frame_tag :command_palette do %>
|
2
|
+
<div data-command-palette-target="content">
|
3
|
+
<div class="kommandant-flex kommandant-px-4">
|
4
|
+
<div class="kommandant-flex kommandant-items-center kommandant-space-x-1 kommandant-text-gray-400">
|
5
|
+
<%= link_to(Kommandant::Engine.routes.url_helpers.searches_path, class: "kommandant-inline-flex kommandant-items-center kommandant-space-x-2 kommandant-border kommandant-shadow-sm kommandant-font-medium kommandant-rounded-md focus:kommandant-outline-none focus:kommandant-ring-2 focus:kommandant-ring-offset-2 disabled:kommandant-opacity-50 disabled:kommandant-cursor-not-allowed kommandant-border-gray-300 kommandant-text-gray-700 kommandant-bg-white hover:kommandant-bg-gray-50 hover:kommandant-cursor-pointer focus:kommandant-ring-gray-500 kommandant-text-xs kommandant-px-2.5 kommandant-py-1.5") do %>
|
6
|
+
<span><%= @command.name %></span>
|
7
|
+
<% end %>
|
8
|
+
|
9
|
+
<%= render "kommandant/shared/icons/chevron_right" %>
|
10
|
+
</div>
|
11
|
+
|
12
|
+
<%= form_with url: Kommandant::Engine.routes.url_helpers.command_searches_path(@command.id), method: :get, class: "kommandant-flex-grow" do |form| %>
|
13
|
+
<%= form.text_field :query, class: "kommandant-h-12 kommandant-w-full kommandant-border-0 kommandant-bg-transparent kommandant-pl-1 kommandant-pr-4 kommandant-text-gray-800 kommandant-placeholder-gray-400 focus:kommandant-ring-0 sm:kommandant-text-sm", value: params[:query], placeholder: @command.placeholder, autocomplete: "off", data: {command_palette_target: "input", action: "keyup->command-palette#submit"} %>
|
14
|
+
<% end %>
|
15
|
+
</div>
|
16
|
+
|
17
|
+
<% if @results.any? %>
|
18
|
+
<ul class="kommandant-overflow-y-auto kommandant-p-2 kommandant-text-sm kommandant-text-gray-700">
|
19
|
+
<% @results.each do |result| %>
|
20
|
+
<%# TODO: How about a default? %>
|
21
|
+
<% if partial_exists_for_result?(result) %>
|
22
|
+
<%= render result.resource, result: result %>
|
23
|
+
<% else %>
|
24
|
+
<%= render "kommandant/shared/command_palette/result", result: result %>
|
25
|
+
<% end %>
|
26
|
+
<% end %>
|
27
|
+
</ul>
|
28
|
+
|
29
|
+
<% if Kommandant.config.pagination.enabled %>
|
30
|
+
<div class="kommandant-px-4 kommandant-py-2 kommandant-flex kommandant-justify-between kommandant-items-baseline">
|
31
|
+
<span class="text-sm"><%= @pagination_info_label %></span>
|
32
|
+
|
33
|
+
<div class="kommandant-space-x-4">
|
34
|
+
<%= link_to(t("kommandant.pagination.prev"), Kommandant::Engine.routes.url_helpers.command_searches_path(@command.id, page: @pagination.prev, query: @query), class: "kommandant-font-medium kommandant-text-sm kommandant-text-gray-600 hover:kommandant-text-gray-800", data: {command_palette_target: "previousPageLink"}) if @pagination.prev %>
|
35
|
+
<%= link_to(t("kommandant.pagination.next"), Kommandant::Engine.routes.url_helpers.command_searches_path(@command.id, page: @pagination.next, query: @query), class: "kommandant-font-medium kommandant-text-sm kommandant-text-gray-600 hover:kommandant-text-gray-800", data: {command_palette_target: "nextPageLink"}) if @pagination.next %>
|
36
|
+
</div>
|
37
|
+
</div>
|
38
|
+
<% end %>
|
39
|
+
<% else %>
|
40
|
+
<%= render "kommandant/shared/command_palette/empty_state", query: params[:query] %>
|
41
|
+
<% end %>
|
42
|
+
</div>
|
43
|
+
|
44
|
+
<%= render "kommandant/shared/command_palette/loading_message" %>
|
45
|
+
|
46
|
+
<% if Kommandant.config.pagination.enabled && @pagination.pages > 1 %>
|
47
|
+
<div class="kommandant-flex kommandant-justify-end kommandant-items-center kommandant-divide-x kommandant-bg-gray-50 kommandant-py-2.5 kommandant-px-4 kommandant-text-xs kommandant-text-gray-700">
|
48
|
+
<div class="kommandant-pr-2 kommandant-flex kommandant-flex-wrap kommandant-justify-end kommandant-items-center kommandant-space-x-2">
|
49
|
+
<span><%= t("kommandant.pagination.prev").html_safe %>:</span>
|
50
|
+
|
51
|
+
<div class="kommandant-flex kommandant-items-center kommandant-space-x-1">
|
52
|
+
<kbd class="kommandant-px-1 kommandant-py-0.5 kommandant-rounded kommandant-border kommandant-border-gray-400 kommandant-bg-white kommandant-font-semibold kommandant-text-gray-900">Ctrl</kbd>
|
53
|
+
<span>+</span>
|
54
|
+
<kbd class="kommandant-px-1 kommandant-py-0.5 kommandant-rounded kommandant-border kommandant-border-gray-400 kommandant-bg-white kommandant-font-semibold kommandant-text-gray-900">J</kbd>
|
55
|
+
</div>
|
56
|
+
</div>
|
57
|
+
|
58
|
+
<div class="kommandant-pl-2 kommandant-flex kommandant-flex-wrap kommandant-justify-end kommandant-items-center kommandant-space-x-2">
|
59
|
+
<span><%= t("kommandant.pagination.next").html_safe %>:</span>
|
60
|
+
|
61
|
+
<div class="kommandant-flex kommandant-items-center kommandant-space-x-1">
|
62
|
+
<kbd class="kommandant-px-1 kommandant-py-0.5 kommandant-rounded kommandant-border kommandant-border-gray-400 kommandant-bg-white kommandant-font-semibold kommandant-text-gray-900">Ctrl</kbd>
|
63
|
+
<span>+</span>
|
64
|
+
<kbd class="kommandant-px-1 kommandant-py-0.5 kommandant-rounded kommandant-border kommandant-border-gray-400 kommandant-bg-white kommandant-font-semibold kommandant-text-gray-900">L</kbd>
|
65
|
+
</div>
|
66
|
+
</div>
|
67
|
+
</div>
|
68
|
+
<% end %>
|
69
|
+
<% end %>
|
@@ -0,0 +1,20 @@
|
|
1
|
+
<%= turbo_frame_tag :command_palette do %>
|
2
|
+
<div data-command-palette-target="content">
|
3
|
+
<div class="kommandant-flex kommandant-px-4">
|
4
|
+
<div class="kommandant-flex kommandant-items-center kommandant-space-x-1 kommandant-text-gray-400">
|
5
|
+
<%= link_to(Kommandant::Engine.routes.url_helpers.searches_path, class: "kommandant-inline-flex kommandant-items-center kommandant-space-x-2 kommandant-border kommandant-shadow-sm kommandant-font-medium kommandant-rounded-md focus:kommandant-outline-none focus:kommandant-ring-2 focus:kommandant-ring-offset-2 disabled:kommandant-opacity-50 disabled:kommandant-cursor-not-allowed kommandant-border-gray-300 kommandant-text-gray-700 kommandant-bg-white hover:kommandant-bg-gray-50 hover:kommandant-cursor-pointer focus:kommandan
|
6
|
+
tring-gray-500 kommandant-text-xs kommandant-px-2.5 kommandant-py-1.5") do %>
|
7
|
+
<span><%= @command.name %></span>
|
8
|
+
<% end %>
|
9
|
+
|
10
|
+
<%= render "kommandant/shared/icons/chevron_right" %>
|
11
|
+
</div>
|
12
|
+
|
13
|
+
<%= form_with url: Kommandant::Engine.routes.url_helpers.command_searches_path(@command.id), method: :get, class: "kommandant-flex-grow" do |form| %>
|
14
|
+
<%= form.text_field :query, class: "kommandant-h-12 kommandant-w-full kommandant-border-0 kommandant-bg-transparent kommandant-pl-1 kommandant-pr-4 kommandant-text-gray-800 kommandant-placeholder-gray-400 focus:kommandant-ring-0 sm:kommandant-text-sm", value: @query, placeholder: @command.placeholder, autocomplete: "off", data: {command_palette_target: "input", action: "keyup->command-palette#submit"} %>
|
15
|
+
<% end %>
|
16
|
+
</div>
|
17
|
+
</div>
|
18
|
+
|
19
|
+
<%= render "kommandant/shared/command_palette/loading_message" %>
|
20
|
+
<% end %>
|
@@ -0,0 +1 @@
|
|
1
|
+
<%= render "kommandant/shared/command_palette", results: @results, query: params[:query] %>
|
@@ -0,0 +1 @@
|
|
1
|
+
<%= render "kommandant/shared/command_palette", recent_commands: @recent_commands %>
|
@@ -0,0 +1,53 @@
|
|
1
|
+
<div class="kommandant-relative kommandant-z-50" role="dialog" aria-modal="true" data-action="click->command-palette#hideWithBackgroundOverlay keydown.ctrl+l->command-palette#nextPage keydown.ctrl+j->command-palette#previousPage keydown.meta+l->command-palette#nextPage keydown.meta+j->command-palette#previousPage">
|
2
|
+
<div class="hidden kommandant-fixed kommandant-inset-0 kommandant-bg-gray-500 kommandant-bg-opacity-25 kommandant-transition-opacity"
|
3
|
+
data-command-palette-target="background"
|
4
|
+
data-transition-enter="kommandant-ease-out kommandant-duration-300"
|
5
|
+
data-transition-enter-start="kommandant-opacity-0"
|
6
|
+
data-transition-enter-end="kommandant-opacity-100"
|
7
|
+
data-transition-leave="kommandant-ease-in kommandant-duration-200"
|
8
|
+
data-transition-leave-start="kommandant-opacity-100"
|
9
|
+
data-transition-leave-end="kommandant-opacity-0">
|
10
|
+
</div>
|
11
|
+
|
12
|
+
<div class="hidden kommandant-fixed kommandant-inset-0 kommandant-z-10 kommandant-overflow-y-auto kommandant-p-4 sm:kommandant-p-6 md:kommandant-p-20"
|
13
|
+
data-command-palette-target="panel"
|
14
|
+
data-transition-enter="kommandant-ease-out kommandant-duration-300"
|
15
|
+
data-transition-enter-start="kommandant-opacity-0 kommandant-scale-95"
|
16
|
+
data-transition-enter-end="kommandant-opacity-100 kommandant-scale-100"
|
17
|
+
data-transition-leave="kommandant-ease-in kommandant-duration-200"
|
18
|
+
data-transition-leave-start="kommandant-opacity-100 kommandant-scale-100"
|
19
|
+
data-transition-leave-end="kommandant-opacity-0 kommandant-scale-95">
|
20
|
+
|
21
|
+
<div class="kommandant-mx-auto kommandant-max-w-2xl kommandant-transform kommandant-divide-y kommandant-divide-gray-100 kommandant-overflow-hidden kommandant-rounded-xl kommandant-bg-white kommandant-shadow-2xl kommandant-ring-1 kommandant-ring-black kommandant-ring-opacity-5 kommandant-transition-all">
|
22
|
+
<%= turbo_frame_tag :command_palette do %>
|
23
|
+
<div data-command-palette-target="content">
|
24
|
+
<div class="kommandant-relative">
|
25
|
+
<div class="kommandant-pointer-events-none kommandant-absolute kommandant-top-3 kommandant-left-4 kommandant-text-gray-400">
|
26
|
+
<%= render "kommandant/shared/icons/search" %>
|
27
|
+
</div>
|
28
|
+
<%= form_with url: Kommandant::Engine.routes.url_helpers.searches_path, method: :get do |form| %>
|
29
|
+
<%= form.text_field :query, value: local_assigns[:query], class: "kommandant-h-12 kommandant-w-full kommandant-border-0 kommandant-bg-transparent kommandant-pl-11 kommandant-pr-4 kommandant-text-gray-800 kommandant-placeholder-gray-400 focus:kommandant-ring-0 sm:kommandant-text-sm", placeholder: t(".input_placeholder"), autocomplete: "off", data: {command_palette_target: "input", action: "keyup->command-palette#submit"} %>
|
30
|
+
<% end %>
|
31
|
+
</div>
|
32
|
+
<% if local_assigns[:results] %>
|
33
|
+
<% if local_assigns[:results].empty? && local_assigns[:query] %>
|
34
|
+
<%= render "kommandant/shared/command_palette/empty_state", query: query %>
|
35
|
+
<% elsif local_assigns[:results].any? %>
|
36
|
+
<!-- Results, show/hide based on command palette state. -->
|
37
|
+
<ul class="kommandant-max-h-96 kommandant-overflow-y-auto kommandant-p-2 kommandant-text-sm kommandant-text-gray-700">
|
38
|
+
<% local_assigns[:results].each do |result| %>
|
39
|
+
<%= render "kommandant/shared/command_palette/command", command: result %>
|
40
|
+
<% end %>
|
41
|
+
</ul>
|
42
|
+
<% end %>
|
43
|
+
<% else %>
|
44
|
+
<%= render "kommandant/shared/command_palette/default_state", recent_commands: local_assigns[:recent_commands] %>
|
45
|
+
<% end %>
|
46
|
+
</div>
|
47
|
+
|
48
|
+
<%= render "kommandant/shared/command_palette/loading_message" %>
|
49
|
+
<% end %>
|
50
|
+
</div>
|
51
|
+
</div>
|
52
|
+
</div>
|
53
|
+
|
@@ -0,0 +1,14 @@
|
|
1
|
+
<li>
|
2
|
+
<%= link_to command.path, class: "kommandant-group kommandant-cursor-default kommandant-select-none kommandant-flex kommandant-items-center kommandant-px-3 kommandant-py-2 kommandant-rounded-md data-[active=true]:kommandant-bg-slate-600 data-[active=true]:kommandant-text-white group-data-[active=true]:kommandant-outline-none", data: {active: false, keyboard_navigation_target: "focusable", action: "mouseenter->keyboard-navigation#focus click->command-palette#showLoadingMessage", turbo_method: command.http_method} do %>
|
3
|
+
<% if command.icon? %>
|
4
|
+
<!-- For some reason the line below works, but the uncommented line does not color the icon... I have no idea why... -->
|
5
|
+
<!-- <div class="kommandant-flex-none kommandant-text-gray-400 group-data-[active=true]:text-white"> -->
|
6
|
+
<div class="kommandant-flex-none kommandant-text-gray-400 group-data-[active=true]:kommandant-text-white">
|
7
|
+
<%= render "kommandant/shared/icons/command", icon: command.icon %>
|
8
|
+
</div>
|
9
|
+
<% end %>
|
10
|
+
|
11
|
+
<span class="kommandant-ml-3 kommandant-flex-auto kommandant-truncate"><%= command.name %></span>
|
12
|
+
<span class="kommandant-ml-3 kommandant-hidden group-data[active=true]:kommandant-inline kommandant-flex-none kommandant-text-indigo-100"><%= t("kommandant.navigation.jump_to") %></span>
|
13
|
+
<% end %>
|
14
|
+
</li>
|
@@ -0,0 +1,14 @@
|
|
1
|
+
<% if local_assigns[:recent_commands] %>
|
2
|
+
<ul class="kommandant-max-h-80 kommandant-scroll-py-2 kommandant-divide-y kommandant-divide-gray-100 kommandant-overflow-y-auto">
|
3
|
+
<% if recent_commands.any? %>
|
4
|
+
<li class="kommandant-p-2">
|
5
|
+
<h2 class="kommandant-mt-4 kommandant-mb-2 kommandant-px-3 kommandant-text-xs kommandant-font-semibold kommandant-text-gray-500"><%= t(".recent_commands") %></h2>
|
6
|
+
<ul class="kommandant-text-sm kommandant-text-gray-700">
|
7
|
+
<% recent_commands.each do |command| %>
|
8
|
+
<%= render "kommandant/shared/command_palette/command", command: command %>
|
9
|
+
<% end %>
|
10
|
+
</ul>
|
11
|
+
</li>
|
12
|
+
<% end %>
|
13
|
+
</ul>
|
14
|
+
<% end %>
|
@@ -0,0 +1,7 @@
|
|
1
|
+
<div class="kommandant-py-14 kommandant-px-6 kommandant-text-center sm:kommandant-px-14">
|
2
|
+
<span class="kommandant-p-2 kommandant-shadow-sm kommandant-inline-flex kommandant-items-center kommandant-justify-center kommandant-text-xs kommandant-leading-5 kommandant-font-semibold kommandant-rounded-full kommandant-flex-shrink-0 kommandant-h-10 kommandant-w-10 kommandant-bg-slate-700 kommandant-text-white">
|
3
|
+
<%= render "kommandant/shared/icons/search" %>
|
4
|
+
</span>
|
5
|
+
|
6
|
+
<p class="kommandant-mt-4 kommandant-text-sm kommandant-text-gray-900"><%= t(".text") %> <span class="kommandant-font-medium"><%= query %></span></p>
|
7
|
+
</div>
|
@@ -0,0 +1,7 @@
|
|
1
|
+
<div data-command-palette-target="loadingMessage" class="hidden">
|
2
|
+
<div class="kommandant-flex kommandant-items-center kommandant-space-x-2 kommandant-px-4 kommandant-h-12">
|
3
|
+
<%= render "kommandant/shared/icons/spinner" %>
|
4
|
+
|
5
|
+
<p class="kommandant-text-sm kommandant-font-medium kommandant-text-gray-700"><%= t(".text") %></p>
|
6
|
+
</div>
|
7
|
+
</div>
|
@@ -0,0 +1,6 @@
|
|
1
|
+
<li>
|
2
|
+
<%= link_to result.path, data: {active: false, keyboard_navigation_target: "focusable", action: "mouseenter->keyboard-navigation#focus click->command-palette#showLoadingMessage", turbo_method: result.http_method, turbo_frame: "_top"}, class: "kommandant-group kommandant-cursor-default kommandant-select-none kommandant-flex kommandant-items-center kommandant-px-3 kommandant-py-2 kommandant-rounded-md data-[active=true]:kommandant-bg-slate-600 data-[active=true]:kommandant-text-white group-data-[active=true]:kommandant-outline-none" do %>
|
3
|
+
<span class="kommandant-ml-3 kommandant-flex-auto kommandant-truncate"><%= result.name %></span>
|
4
|
+
<span class="kommandant-ml-3 kommandant-hidden group-data-[active=true]:kommandant-inline kommandant-flex-none kommandant-text-indigo-100"><%= t("kommandant.navigation.jump_to") %></span>
|
5
|
+
<% end %>
|
6
|
+
</li>
|
@@ -0,0 +1,3 @@
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="kommandant-w-6 kommandant-h-6">
|
2
|
+
<path fill-rule="evenodd" d="M16.28 11.47a.75.75 0 010 1.06l-7.5 7.5a.75.75 0 01-1.06-1.06L14.69 12 7.72 5.03a.75.75 0 011.06-1.06l7.5 7.5z" clip-rule="evenodd" />
|
3
|
+
</svg>
|
@@ -0,0 +1,7 @@
|
|
1
|
+
<div class="kommandant-flex kommandant-space-x-2">
|
2
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="kommandant-w-5 kommandant-h-5">
|
3
|
+
<path fill-rule="evenodd" d="M8.485 2.495c.673-1.167 2.357-1.167 3.03 0l6.28 10.875c.673 1.167-.17 2.625-1.516 2.625H3.72c-1.347 0-2.189-1.458-1.515-2.625L8.485 2.495zM10 5a.75.75 0 01.75.75v3.5a.75.75 0 01-1.5 0v-3.5A.75.75 0 0110 5zm0 9a1 1 0 100-2 1 1 0 000 2z" clip-rule="evenodd" />
|
4
|
+
</svg>
|
5
|
+
|
6
|
+
<span><%= t(".icon_missing", icon_name: icon) %></span>
|
7
|
+
</div>
|
@@ -0,0 +1,3 @@
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="kommandant-w-5 kommandant-h-5">
|
2
|
+
<path fill-rule="evenodd" d="M9 3.5a5.5 5.5 0 100 11 5.5 5.5 0 000-11zM2 9a7 7 0 1112.452 4.391l3.328 3.329a.75.75 0 11-1.06 1.06l-3.329-3.328A7 7 0 012 9z" clip-rule="evenodd" />
|
3
|
+
</svg>
|
@@ -0,0 +1,4 @@
|
|
1
|
+
<svg class="kommandant-animate-spin kommandant-h-5 kommandant-w-5 kommandant-text-gray-900" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
2
|
+
<circle class="kommandant-opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
3
|
+
<path class="kommandant-opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
4
|
+
</svg>
|
@@ -0,0 +1,19 @@
|
|
1
|
+
da:
|
2
|
+
kommandant:
|
3
|
+
shared:
|
4
|
+
command_palette:
|
5
|
+
input_placeholder: Søg
|
6
|
+
default_state:
|
7
|
+
recent_commands: Sidst brugte
|
8
|
+
empty_state:
|
9
|
+
text: Vi kunne ikke finde nogen resultater for
|
10
|
+
loading_message:
|
11
|
+
text: Henter...
|
12
|
+
icons:
|
13
|
+
command:
|
14
|
+
icon_missing: Command icon mangler med navn '%{icon_name}'
|
15
|
+
navigation:
|
16
|
+
jump_to: Gå til
|
17
|
+
pagination:
|
18
|
+
prev: Forrige
|
19
|
+
next: Næste
|
@@ -0,0 +1,19 @@
|
|
1
|
+
en:
|
2
|
+
kommandant:
|
3
|
+
shared:
|
4
|
+
command_palette:
|
5
|
+
input_placeholder: Search
|
6
|
+
default_state:
|
7
|
+
recent_commands: Recently used
|
8
|
+
empty_state:
|
9
|
+
text: No results found for
|
10
|
+
loading_message:
|
11
|
+
text: Loading...
|
12
|
+
icons:
|
13
|
+
command:
|
14
|
+
icon_missing: Command icon missing with name '%{icon_name}'
|
15
|
+
navigation:
|
16
|
+
jump_to: Jump to
|
17
|
+
pagination:
|
18
|
+
prev: Previous
|
19
|
+
next: Next
|
data/config/routes.rb
CHANGED
data/lib/kommandant/engine.rb
CHANGED
@@ -1,5 +1,12 @@
|
|
1
1
|
module Kommandant
|
2
2
|
class Engine < ::Rails::Engine
|
3
3
|
isolate_namespace Kommandant
|
4
|
+
|
5
|
+
PRECOMPILE_ASSETS = %w( kommandant.js )
|
6
|
+
initializer "kommandant.assets" do |app|
|
7
|
+
if Rails.application.config.respond_to?(:assets)
|
8
|
+
Rails.application.config.assets.precompile += PRECOMPILE_ASSETS
|
9
|
+
end
|
10
|
+
end
|
4
11
|
end
|
5
12
|
end
|
data/lib/kommandant/version.rb
CHANGED
data/lib/kommandant.rb
CHANGED
@@ -1,6 +1,26 @@
|
|
1
1
|
require "kommandant/version"
|
2
2
|
require "kommandant/engine"
|
3
|
+
require "meilisearch-rails"
|
4
|
+
require "dry-configurable"
|
3
5
|
|
4
6
|
module Kommandant
|
5
|
-
|
7
|
+
extend Dry::Configurable
|
8
|
+
|
9
|
+
setting :commands_path, default: "config/kommandant/commands.json"
|
10
|
+
setting :search_result_filter_lambda
|
11
|
+
setting :admin_only_filter_lambda
|
12
|
+
setting :parent_controller, default: "::ApplicationController"
|
13
|
+
setting :current_user_method, default: "current_user"
|
14
|
+
setting :recent_commands do
|
15
|
+
setting :enabled, default: true
|
16
|
+
end
|
17
|
+
setting :pagination do
|
18
|
+
setting :enabled, default: true
|
19
|
+
setting :items_per_page, default: 10
|
20
|
+
setting :pagination_lambda, default: ->(results, items, controller) { controller.send(:pagy_array, results, items: items) }
|
21
|
+
setting :info_label_lambda, default: ->(pagination, controller) { controller.send(:pagy_info, pagination).html_safe }
|
22
|
+
setting :module, default: "Pagy::Frontend"
|
23
|
+
end
|
24
|
+
|
25
|
+
autoload :Command, "kommandant/command"
|
6
26
|
end
|
@@ -0,0 +1,120 @@
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
2
|
+
import { enter, leave, toggle } from "./transition"
|
3
|
+
|
4
|
+
export default class extends Controller {
|
5
|
+
static targets = ["background", "panel", "input", "content", "loadingMessage", "previousPageLink", "nextPageLink"]
|
6
|
+
static values = {
|
7
|
+
open: { type: Boolean, default: false },
|
8
|
+
loadingMessageDelay: { type: Number, default: 100 },
|
9
|
+
animationDuration: { type: Number, default: 200 },
|
10
|
+
modifierKeyDown: { type: Boolean, default: false }
|
11
|
+
}
|
12
|
+
|
13
|
+
initialize() {
|
14
|
+
this.submit = this.debounce(this.submit, 500).bind(this);
|
15
|
+
}
|
16
|
+
|
17
|
+
toggle(event) {
|
18
|
+
event.preventDefault()
|
19
|
+
|
20
|
+
toggle(this.backgroundTarget)
|
21
|
+
toggle(this.panelTarget)
|
22
|
+
this.openValue = !this.openValue
|
23
|
+
}
|
24
|
+
|
25
|
+
show(event) {
|
26
|
+
event.preventDefault()
|
27
|
+
|
28
|
+
enter(this.backgroundTarget)
|
29
|
+
enter(this.panelTarget)
|
30
|
+
this.openValue = true
|
31
|
+
}
|
32
|
+
|
33
|
+
hide(event) {
|
34
|
+
event.preventDefault()
|
35
|
+
|
36
|
+
leave(this.backgroundTarget)
|
37
|
+
leave(this.panelTarget)
|
38
|
+
this.openValue = false
|
39
|
+
}
|
40
|
+
|
41
|
+
hideWithBackgroundOverlay(event) {
|
42
|
+
if (event.target === this.panelTarget) {
|
43
|
+
this.hide(event)
|
44
|
+
}
|
45
|
+
}
|
46
|
+
|
47
|
+
submit(event) {
|
48
|
+
const ignoreKeys = ["Enter", "Tab", "Escape", "ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight", "Home", "End", "Alt", "Control", "Meta", "Shift"]
|
49
|
+
const modifierKeyPressed = event.ctrlKey || event.metaKey
|
50
|
+
|
51
|
+
if (!modifierKeyPressed && !ignoreKeys.includes(event.key) && this.inputTarget.value !== "") {
|
52
|
+
this.inputTarget.form.requestSubmit()
|
53
|
+
}
|
54
|
+
}
|
55
|
+
|
56
|
+
reset() {
|
57
|
+
setTimeout(() => {
|
58
|
+
Turbo.visit("/kommandant/searches/new", { frame: "command_palette" })
|
59
|
+
}, this.animationDurationValue)
|
60
|
+
}
|
61
|
+
|
62
|
+
showLoadingMessage() {
|
63
|
+
this.loadingTimeout = setTimeout(() => {
|
64
|
+
this.contentTarget.classList.add("hidden")
|
65
|
+
this.loadingMessageTarget.classList.remove("hidden")
|
66
|
+
}, this.loadingMessageDelayValue)
|
67
|
+
}
|
68
|
+
|
69
|
+
nextPage(event) {
|
70
|
+
event.preventDefault()
|
71
|
+
|
72
|
+
if (this.hasNextPageLinkTarget) {
|
73
|
+
this.nextPageLinkTarget.click()
|
74
|
+
}
|
75
|
+
}
|
76
|
+
|
77
|
+
previousPage(event) {
|
78
|
+
event.preventDefault()
|
79
|
+
|
80
|
+
if (this.hasPreviousPageLinkTarget) {
|
81
|
+
this.previousPageLinkTarget.click()
|
82
|
+
}
|
83
|
+
}
|
84
|
+
|
85
|
+
// Private
|
86
|
+
|
87
|
+
openValueChanged(value, previousValue) {
|
88
|
+
this.focusInputTarget()
|
89
|
+
|
90
|
+
if (value === false && previousValue !== undefined) {
|
91
|
+
this.reset()
|
92
|
+
}
|
93
|
+
}
|
94
|
+
|
95
|
+
inputTargetConnected() {
|
96
|
+
this.focusInputTarget()
|
97
|
+
clearTimeout(this.loadingTimeout)
|
98
|
+
}
|
99
|
+
|
100
|
+
focusInputTarget() {
|
101
|
+
if (this.openValue) {
|
102
|
+
this.inputTarget.focus()
|
103
|
+
|
104
|
+
// Set cursor at end of inputted text
|
105
|
+
let temp = this.inputTarget.value
|
106
|
+
this.inputTarget.value = ""
|
107
|
+
this.inputTarget.value = temp
|
108
|
+
}
|
109
|
+
}
|
110
|
+
|
111
|
+
debounce(func, delay) {
|
112
|
+
let timeoutId;
|
113
|
+
return function (...args) {
|
114
|
+
clearTimeout(timeoutId);
|
115
|
+
timeoutId = setTimeout(() => {
|
116
|
+
func.apply(this, args);
|
117
|
+
}, delay);
|
118
|
+
};
|
119
|
+
}
|
120
|
+
}
|