kommandant 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +230 -12
  3. data/app/assets/builds/kommandant.css +413 -145
  4. data/app/assets/config/kommandant_manifest.js +2 -0
  5. data/app/assets/images/kommandant/logo.svg +13 -0
  6. data/app/controllers/kommandant/commands_controller.rb +69 -11
  7. data/app/controllers/kommandant/searches_controller.rb +8 -9
  8. data/app/models/kommandant/command.rb +12 -69
  9. data/app/models/kommandant/commands/search_result.rb +4 -11
  10. data/app/views/kommandant/commands/_command.html.erb +23 -0
  11. data/app/views/kommandant/commands/_form.html.erb +58 -0
  12. data/app/views/kommandant/commands/edit.html.erb +1 -0
  13. data/app/views/kommandant/commands/index.html.erb +29 -0
  14. data/app/views/kommandant/commands/new.html.erb +1 -0
  15. data/app/views/kommandant/commands/searches/show.html.erb +4 -4
  16. data/app/views/kommandant/commands/show.html.erb +2 -2
  17. data/app/views/kommandant/shared/_command_palette.html.erb +2 -3
  18. data/app/views/kommandant/shared/command_palette/_command.html.erb +11 -4
  19. data/app/views/kommandant/shared/command_palette/_result.html.erb +5 -1
  20. data/app/views/kommandant/shared/icons/_kommandant.html.erb +5 -0
  21. data/app/views/layouts/kommandant/application.html.erb +24 -9
  22. data/config/locales/en.yml +11 -0
  23. data/config/routes.rb +1 -1
  24. data/lib/generators/kommandant/USAGE +9 -0
  25. data/lib/generators/kommandant/install_generator.rb +9 -0
  26. data/lib/generators/kommandant/templates/initializer.rb +31 -0
  27. data/lib/kommandant/engine.rb +2 -2
  28. data/lib/kommandant/version.rb +1 -1
  29. data/lib/kommandant.rb +4 -0
  30. data/lib/tasks/kommandant_tasks.rake +31 -4
  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 +36 -22
  36. /data/app/views/kommandant/shared/icons/{_command.erb → _command.html.erb} +0 -0
  37. /data/app/views/kommandant/shared/icons/{_search.erb → _search.html.erb} +0 -0
  38. /data/app/views/kommandant/shared/icons/{_spinner.erb → _spinner.html.erb} +0 -0
@@ -0,0 +1,31 @@
1
+ Kommandant.configure do |config|
2
+ # Commands are loaded from a JSON file. "config/kommandant/commands.json" is the default path. If you want to use a different path, you can set it here.
3
+ # config.commands_path = "your/custom/path"
4
+
5
+ # When meilisearch returns a result, it might include items, that the current user is not allowed to see. You can filter these results with a lamda. This setting has no default, but below is an example that works with cancancan.
6
+ # config.search_result_filter_lambda = ->(current_ability, resource) { current_ability.can?(:show, resource) }
7
+
8
+ # Another search result filter. We use this to allow admins, who are impersonating users, access to all their commands. This setting has no default.
9
+ # config.admin_only_filter_lambda = ->(current_user, current_admin) { current_user.admin? || current_admin }
10
+
11
+ # If you want Kommandant to use a different parent controller, this is the setting for you. Defaults to ApplicationController
12
+ # config :parent_controller = "YourVerySpecialController"
13
+
14
+ # We assume there is a logged in user and therefore a current_user method. The name of this method can be set here. It defaults to current_user.
15
+ # config.current_user_method = current_account
16
+
17
+ # If you use Kredis, Kommandant can display the current user's most recently used commands. It requires your user model to have kredis_unique_list called recent_commands. If you don't use Kredis or do not want this behavior, it can be disabled. It defaults to being enabled.
18
+ # class User < ApplicationRecord
19
+ # kredis_unique_list :recent_commands, limit: 5
20
+ # config.recent_commands.enabled = false
21
+
22
+ # When a search returns a lot of results, it can be useful to paginate them. We use Pagy by default to handle this. If you do not use Pagy, this functinality can be turned off or configured to suit your needs. It defaults to being enabled.
23
+ # config.pagination.enabled = false
24
+ # config.pagination.items_per_page = 10 # defaults to 10
25
+ # config.pagination.pagination_lambda = ->(results, items, controller) { controller.send(:pagy_array, results, items: items) }
26
+ # config.pagination.info_label_lambda = ->(pagination, controller) { controller.send(:pagy_info, pagination).html_safe }
27
+ # config.pagination.module = "Pagy::Frontend"
28
+
29
+ # If you don't want to use icons, you can disable them here. It defaults to being enabled.
30
+ # config.icons.enabled = false
31
+ end
@@ -1,8 +1,8 @@
1
1
  module Kommandant
2
2
  class Engine < ::Rails::Engine
3
3
  isolate_namespace Kommandant
4
-
5
- PRECOMPILE_ASSETS = %w( kommandant.js )
4
+
5
+ PRECOMPILE_ASSETS = %w[kommandant.js kommandant.css]
6
6
  initializer "kommandant.assets" do |app|
7
7
  if Rails.application.config.respond_to?(:assets)
8
8
  Rails.application.config.assets.precompile += PRECOMPILE_ASSETS
@@ -1,3 +1,3 @@
1
1
  module Kommandant
2
- VERSION = "0.2.0"
2
+ VERSION = "0.3.0"
3
3
  end
data/lib/kommandant.rb CHANGED
@@ -11,9 +11,13 @@ module Kommandant
11
11
  setting :admin_only_filter_lambda
12
12
  setting :parent_controller, default: "::ApplicationController"
13
13
  setting :current_user_method, default: "current_user"
14
+ setting :highlight_search_term, default: true
14
15
  setting :recent_commands do
15
16
  setting :enabled, default: true
16
17
  end
18
+ setting :icons do
19
+ setting :enabled, default: true
20
+ end
17
21
  setting :pagination do
18
22
  setting :enabled, default: true
19
23
  setting :items_per_page, default: 10
@@ -1,4 +1,31 @@
1
- # desc "Explaining what the task does"
2
- # task :kommandant do
3
- # # Task goes here
4
- # end
1
+ namespace :kommandant do
2
+ desc "Reindexes all commmands defined in the commands.json file (or whatever you called it)"
3
+ task reindex_commands: :environment do
4
+ puts "Reindexing all Kommandant commands"
5
+
6
+ Kommandant::Command.reindex!
7
+ end
8
+
9
+ desc "Reindexes all models set up with Kommandant"
10
+ task reindex_models: :environment do
11
+ Rake::Task["meilisearch:reindex"].invoke
12
+ end
13
+
14
+ desc "Reindexes all commands and models. We recommend you run this task every time you deploy your application, to avoid any inconsistencies between what you expect to be indexed and what is actually indexed"
15
+ task reindex: [:reindex_commands, :reindex_models]
16
+
17
+ desc "Clears the Kommandant commands from the search engine"
18
+ task clear_command_index: :environment do
19
+ puts "Clearing the Kommandant commands"
20
+
21
+ Kommandant::Command.remove_from_index!
22
+ end
23
+
24
+ desc "Clears the search engine of all models set up with Kommandant"
25
+ task clear_model_index: :environment do
26
+ Rake::Task["meilisearch:clear_indexes"].invoke
27
+ end
28
+
29
+ desc "Clears the search engine of all commands and models"
30
+ task clear_indexes: [:clear_command_index, :clear_model_index]
31
+ 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
+ }
@@ -0,0 +1,72 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ export default class extends Controller {
4
+ static targets = ["focusable"]
5
+ static values = { activeOptionIndex: { type: Number, default: 0 } }
6
+
7
+ connect() {
8
+ if (this.activeOption) {
9
+ this.activateOption()
10
+ }
11
+ }
12
+
13
+ down(event) {
14
+ event.preventDefault()
15
+
16
+ if (this.activeOptionIndexValue < (this.focusableTargets.length - 1)) {
17
+ this.deactivateOption()
18
+ this.activeOptionIndexValue++
19
+ this.activateOption()
20
+ }
21
+ }
22
+
23
+ up(event) {
24
+ event.preventDefault()
25
+
26
+ if (this.activeOptionIndexValue > 0) {
27
+ this.deactivateOption()
28
+ this.activeOptionIndexValue--
29
+ this.activateOption()
30
+ }
31
+ }
32
+
33
+ focus(event) {
34
+ this.deactivateOption()
35
+ this.activeOptionIndexValue = this.focusableTargets.indexOf(event.currentTarget)
36
+ this.activateOption()
37
+ }
38
+
39
+ select(event) {
40
+ if (this.hasFocusableTarget && this.focusableTargetIsVisible) {
41
+ event.preventDefault()
42
+
43
+ this.activeOption.click()
44
+ }
45
+ }
46
+
47
+ // Private
48
+
49
+ deactivateOption() {
50
+ this.activeOption.dataset.active = false
51
+ }
52
+
53
+ activateOption() {
54
+ this.activeOption.dataset.active = true
55
+ }
56
+
57
+ focusableTargetDisconnected() {
58
+ this.activeOptionIndexValue = 0
59
+ }
60
+
61
+ focusableTargetConnected() {
62
+ this.activateOption()
63
+ }
64
+
65
+ get activeOption() {
66
+ return this.focusableTargets[this.activeOptionIndexValue]
67
+ }
68
+
69
+ get focusableTargetIsVisible() {
70
+ return this.focusableTarget.offsetParent !== null
71
+ }
72
+ }
@@ -0,0 +1,2 @@
1
+ export { default as CommandPalette } from './command_palette'
2
+ export { default as KeyboardNavigation } from './keyboard_navigation'
@@ -0,0 +1,57 @@
1
+ // This was taken from https://github.com/mmccall10/el-transition but is included here to avoid the need for a separate package
2
+
3
+ export async function enter(element, transitionName = null) {
4
+ element.classList.remove('hidden')
5
+ await transition('enter', element, transitionName)
6
+ }
7
+
8
+ export async function leave(element, transitionName = null) {
9
+ await transition('leave', element, transitionName)
10
+ element.classList.add('hidden')
11
+ }
12
+
13
+ export async function toggle(element, transitionName = null) {
14
+ if (element.classList.contains('hidden')) {
15
+ await enter(element, transitionName)
16
+ } else {
17
+ await leave(element, transitionName)
18
+ }
19
+ }
20
+
21
+ async function transition(direction, element, animation) {
22
+ const dataset = element.dataset
23
+ const animationClass = animation ? `${animation}-${direction}` : direction
24
+ let transition = `transition${direction.charAt(0).toUpperCase() + direction.slice(1)}`
25
+ const genesis = dataset[transition] ? dataset[transition].split(" ") : [animationClass]
26
+ const start = dataset[`${transition}Start`] ? dataset[`${transition}Start`].split(" ") : [`${animationClass}-start`]
27
+ const end = dataset[`${transition}End`] ? dataset[`${transition}End`].split(" ") : [`${animationClass}-end`]
28
+
29
+ addClasses(element, genesis)
30
+ addClasses(element, start)
31
+ await nextFrame()
32
+ removeClasses(element, start)
33
+ addClasses(element, end);
34
+ await afterTransition(element)
35
+ removeClasses(element, end)
36
+ removeClasses(element, genesis)
37
+ }
38
+
39
+ function addClasses(element, classes) {
40
+ element.classList.add(...classes)
41
+ }
42
+
43
+ function removeClasses(element, classes) {
44
+ element.classList.remove(...classes)
45
+ }
46
+
47
+ function nextFrame() {
48
+ return new Promise(resolve => {
49
+ requestAnimationFrame(() => {
50
+ requestAnimationFrame(resolve)
51
+ });
52
+ });
53
+ }
54
+
55
+ function afterTransition(element) {
56
+ return Promise.all(element.getAnimations().map(animation => animation.finished));
57
+ }
metadata CHANGED
@@ -1,29 +1,35 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: kommandant
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nicolai Bach Woller
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-02-20 00:00:00.000000000 Z
11
+ date: 2024-06-11 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
- name: rails
14
+ name: dry-configurable
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.0'
17
20
  - - ">="
18
21
  - !ruby/object:Gem::Version
19
- version: 7.0.5
22
+ version: 1.0.1
20
23
  type: :runtime
21
24
  prerelease: false
22
25
  version_requirements: !ruby/object:Gem::Requirement
23
26
  requirements:
27
+ - - "~>"
28
+ - !ruby/object:Gem::Version
29
+ version: '1.0'
24
30
  - - ">="
25
31
  - !ruby/object:Gem::Version
26
- version: 7.0.5
32
+ version: 1.0.1
27
33
  - !ruby/object:Gem::Dependency
28
34
  name: meilisearch-rails
29
35
  requirement: !ruby/object:Gem::Requirement
@@ -39,39 +45,33 @@ dependencies:
39
45
  - !ruby/object:Gem::Version
40
46
  version: 0.8.1
41
47
  - !ruby/object:Gem::Dependency
42
- name: turbo-rails
48
+ name: rails
43
49
  requirement: !ruby/object:Gem::Requirement
44
50
  requirements:
45
- - - "~>"
51
+ - - ">="
46
52
  - !ruby/object:Gem::Version
47
- version: '1.4'
53
+ version: 7.0.5
48
54
  type: :runtime
49
55
  prerelease: false
50
56
  version_requirements: !ruby/object:Gem::Requirement
51
57
  requirements:
52
- - - "~>"
58
+ - - ">="
53
59
  - !ruby/object:Gem::Version
54
- version: '1.4'
60
+ version: 7.0.5
55
61
  - !ruby/object:Gem::Dependency
56
- name: dry-configurable
62
+ name: turbo-rails
57
63
  requirement: !ruby/object:Gem::Requirement
58
64
  requirements:
59
65
  - - "~>"
60
66
  - !ruby/object:Gem::Version
61
- version: '1.0'
62
- - - ">="
63
- - !ruby/object:Gem::Version
64
- version: 1.0.1
67
+ version: '2.0'
65
68
  type: :runtime
66
69
  prerelease: false
67
70
  version_requirements: !ruby/object:Gem::Requirement
68
71
  requirements:
69
72
  - - "~>"
70
73
  - !ruby/object:Gem::Version
71
- version: '1.0'
72
- - - ">="
73
- - !ruby/object:Gem::Version
74
- version: 1.0.1
74
+ version: '2.0'
75
75
  - !ruby/object:Gem::Dependency
76
76
  name: tailwindcss-rails
77
77
  requirement: !ruby/object:Gem::Requirement
@@ -98,6 +98,7 @@ files:
98
98
  - Rakefile
99
99
  - app/assets/builds/kommandant.css
100
100
  - app/assets/config/kommandant_manifest.js
101
+ - app/assets/images/kommandant/logo.svg
101
102
  - app/assets/stylesheets/kommandant/application.css
102
103
  - app/assets/stylesheets/kommandant/application.tailwind.css
103
104
  - app/controllers/concerns/kommandant/recent_commands.rb
@@ -111,6 +112,11 @@ files:
111
112
  - app/models/kommandant/application_record.rb
112
113
  - app/models/kommandant/command.rb
113
114
  - app/models/kommandant/commands/search_result.rb
115
+ - app/views/kommandant/commands/_command.html.erb
116
+ - app/views/kommandant/commands/_form.html.erb
117
+ - app/views/kommandant/commands/edit.html.erb
118
+ - app/views/kommandant/commands/index.html.erb
119
+ - app/views/kommandant/commands/new.html.erb
114
120
  - app/views/kommandant/commands/searches/show.html.erb
115
121
  - app/views/kommandant/commands/show.html.erb
116
122
  - app/views/kommandant/searches/index.html.erb
@@ -122,17 +128,25 @@ files:
122
128
  - app/views/kommandant/shared/command_palette/_loading_message.html.erb
123
129
  - app/views/kommandant/shared/command_palette/_result.html.erb
124
130
  - app/views/kommandant/shared/icons/_chevron_right.html.erb
125
- - app/views/kommandant/shared/icons/_command.erb
126
- - app/views/kommandant/shared/icons/_search.erb
127
- - app/views/kommandant/shared/icons/_spinner.erb
131
+ - app/views/kommandant/shared/icons/_command.html.erb
132
+ - app/views/kommandant/shared/icons/_kommandant.html.erb
133
+ - app/views/kommandant/shared/icons/_search.html.erb
134
+ - app/views/kommandant/shared/icons/_spinner.html.erb
128
135
  - app/views/layouts/kommandant/application.html.erb
129
136
  - config/locales/da.yml
130
137
  - config/locales/en.yml
131
138
  - config/routes.rb
139
+ - lib/generators/kommandant/USAGE
140
+ - lib/generators/kommandant/install_generator.rb
141
+ - lib/generators/kommandant/templates/initializer.rb
132
142
  - lib/kommandant.rb
133
143
  - lib/kommandant/engine.rb
134
144
  - lib/kommandant/version.rb
135
145
  - lib/tasks/kommandant_tasks.rake
146
+ - vendor/assets/javascripts/command_palette.js
147
+ - vendor/assets/javascripts/keyboard_navigation.js
148
+ - vendor/assets/javascripts/kommandant.js
149
+ - vendor/assets/javascripts/transition.js
136
150
  homepage: https://traels.it
137
151
  licenses:
138
152
  - MIT