kommandant 0.1.0 → 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (35) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/builds/kommandant.css +634 -0
  3. data/app/assets/stylesheets/kommandant/application.tailwind.css +3 -0
  4. data/app/controllers/concerns/kommandant/recent_commands.rb +21 -0
  5. data/app/controllers/kommandant/application_controller.rb +6 -1
  6. data/app/controllers/kommandant/commands/searches_controller.rb +40 -0
  7. data/app/controllers/kommandant/commands_controller.rb +17 -0
  8. data/app/controllers/kommandant/searches_controller.rb +14 -0
  9. data/app/models/kommandant/command.rb +88 -0
  10. data/app/models/kommandant/commands/search_result.rb +44 -0
  11. data/app/views/kommandant/commands/searches/show.html.erb +69 -0
  12. data/app/views/kommandant/commands/show.html.erb +20 -0
  13. data/app/views/kommandant/searches/index.html.erb +1 -0
  14. data/app/views/kommandant/searches/new.html.erb +1 -0
  15. data/app/views/kommandant/shared/_command_palette.html.erb +53 -0
  16. data/app/views/kommandant/shared/command_palette/_command.html.erb +14 -0
  17. data/app/views/kommandant/shared/command_palette/_default_state.html.erb +14 -0
  18. data/app/views/kommandant/shared/command_palette/_empty_state.html.erb +7 -0
  19. data/app/views/kommandant/shared/command_palette/_loading_message.html.erb +7 -0
  20. data/app/views/kommandant/shared/command_palette/_result.html.erb +6 -0
  21. data/app/views/kommandant/shared/icons/_chevron_right.html.erb +3 -0
  22. data/app/views/kommandant/shared/icons/_command.erb +7 -0
  23. data/app/views/kommandant/shared/icons/_search.erb +3 -0
  24. data/app/views/kommandant/shared/icons/_spinner.erb +4 -0
  25. data/config/locales/da.yml +19 -0
  26. data/config/locales/en.yml +19 -0
  27. data/config/routes.rb +4 -0
  28. data/lib/kommandant/engine.rb +7 -0
  29. data/lib/kommandant/version.rb +1 -1
  30. data/lib/kommandant.rb +21 -1
  31. data/vendor/assets/javascripts/command_palette.js +120 -0
  32. data/vendor/assets/javascripts/keyboard_navigation.js +72 -0
  33. data/vendor/assets/javascripts/kommandant.js +2 -0
  34. data/vendor/assets/javascripts/transition.js +57 -0
  35. 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
@@ -1,2 +1,6 @@
1
1
  Kommandant::Engine.routes.draw do
2
+ resources :searches, only: [:index, :new]
3
+ resources :commands, only: [:show] do
4
+ resource :searches, module: :commands, only: [:show]
5
+ end
2
6
  end
@@ -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
@@ -1,3 +1,3 @@
1
1
  module Kommandant
2
- VERSION = "0.1.0"
2
+ VERSION = "0.2.1"
3
3
  end
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
- # Your code goes here...
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
+ }