ahoy_captain 0.8 → 0.10.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (167) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +25 -13
  3. data/app/assets/javascript/ahoy_captain/application.js +4 -4
  4. data/app/assets/javascript/ahoy_captain/controllers/active_links_controller.js +30 -0
  5. data/app/assets/javascript/ahoy_captain/controllers/application.js +5 -5
  6. data/app/assets/javascript/ahoy_captain/controllers/application_controller.js +16 -9
  7. data/app/assets/javascript/ahoy_captain/controllers/combobox_controller.js +341 -0
  8. data/app/assets/javascript/ahoy_captain/controllers/details_modal_controller.js +5 -5
  9. data/app/assets/javascript/ahoy_captain/controllers/dropdown_label_controller.js +2 -2
  10. data/app/assets/javascript/ahoy_captain/controllers/filter/item_controller.js +12 -0
  11. data/app/assets/javascript/ahoy_captain/controllers/filter_form_controller.js +13 -0
  12. data/app/assets/javascript/ahoy_captain/controllers/filter_modal_controller.js +45 -0
  13. data/app/assets/javascript/ahoy_captain/controllers/funnel_chart_controller.js +116 -104
  14. data/app/assets/javascript/ahoy_captain/controllers/index.js +4 -3
  15. data/app/assets/javascript/ahoy_captain/controllers/interval_controller.js +8 -3
  16. data/app/assets/javascript/ahoy_captain/controllers/line_chart_controller.js +188 -0
  17. data/app/assets/javascript/ahoy_captain/controllers/predicate_select_controller.js +9 -0
  18. data/app/assets/javascript/ahoy_captain/controllers/properties_controller.js +8 -0
  19. data/app/assets/javascript/ahoy_captain/controllers/property_filter_controller.js +46 -0
  20. data/app/assets/javascript/ahoy_captain/controllers/realtime_controller.js +12 -9
  21. data/app/assets/javascript/ahoy_captain/controllers/tile_controller.js +9 -0
  22. data/app/assets/javascript/ahoy_captain/controllers/toggle_controller.js +17 -0
  23. data/app/assets/javascript/ahoy_captain/helpers/chart_utils.js +156 -0
  24. data/app/assets/javascript/ahoy_captain/helpers/number_formatters.js +55 -0
  25. data/app/components/ahoy_captain/combobox_component.html.erb +33 -0
  26. data/app/components/ahoy_captain/combobox_component.rb +13 -0
  27. data/app/components/ahoy_captain/comparison_link_component.rb +40 -0
  28. data/app/components/ahoy_captain/dropdown_button_component.html.erb +5 -5
  29. data/app/components/ahoy_captain/dropdown_link_component.html.erb +5 -5
  30. data/app/components/ahoy_captain/filter/dropdown_component.html.erb +50 -0
  31. data/app/components/ahoy_captain/filter/dropdown_component.rb +51 -0
  32. data/app/components/ahoy_captain/filter/modal_component.html.erb +12 -9
  33. data/app/components/ahoy_captain/filter/select_component.html.erb +23 -19
  34. data/app/components/ahoy_captain/filter/select_component.rb +41 -9
  35. data/app/components/ahoy_captain/filter/tag_component.html.erb +8 -4
  36. data/app/components/ahoy_captain/filter/tag_component.rb +6 -30
  37. data/app/components/ahoy_captain/filter/tag_container_component.html.erb +2 -3
  38. data/app/components/ahoy_captain/filter/tag_container_component.rb +1 -8
  39. data/app/components/ahoy_captain/stats/comparable_container_component.html.erb +25 -0
  40. data/app/components/ahoy_captain/stats/comparable_container_component.rb +86 -0
  41. data/app/components/ahoy_captain/stats/container_component.html.erb +15 -0
  42. data/app/components/ahoy_captain/stats/container_component.rb +26 -0
  43. data/app/components/ahoy_captain/sticky_nav_component.html.erb +13 -22
  44. data/app/components/ahoy_captain/sticky_nav_component.rb +11 -0
  45. data/app/components/ahoy_captain/table_component.html.erb +4 -37
  46. data/app/components/ahoy_captain/table_component.rb +25 -5
  47. data/app/components/ahoy_captain/tables/devices_table_component.rb +11 -0
  48. data/app/components/ahoy_captain/tables/dynamic_table.rb +13 -0
  49. data/app/components/ahoy_captain/tables/dynamic_table_component.rb +204 -0
  50. data/app/components/ahoy_captain/tables/goals_table_component.rb +17 -0
  51. data/app/components/ahoy_captain/tables/header_component.html.erb +5 -0
  52. data/app/components/ahoy_captain/tables/header_component.rb +18 -0
  53. data/app/components/ahoy_captain/tables/headers/header_component.html.erb +5 -0
  54. data/app/components/ahoy_captain/tables/headers/header_component.rb +16 -0
  55. data/app/components/ahoy_captain/tables/properties_table_component.rb +27 -0
  56. data/app/components/ahoy_captain/tables/row_component.html.erb +4 -0
  57. data/app/components/ahoy_captain/tables/rows/row_component.html.erb +6 -0
  58. data/app/components/ahoy_captain/tables/rows/row_component.rb +40 -0
  59. data/app/components/ahoy_captain/tile_component.html.erb +21 -10
  60. data/app/components/ahoy_captain/tile_component.rb +3 -2
  61. data/app/components/ahoy_captain/tooltip_component.html.erb +2 -2
  62. data/app/controllers/ahoy_captain/application_controller.rb +19 -29
  63. data/app/controllers/ahoy_captain/campaigns_controller.rb +2 -10
  64. data/app/controllers/ahoy_captain/cities_controller.rb +2 -6
  65. data/app/controllers/ahoy_captain/countries_controller.rb +2 -6
  66. data/app/controllers/ahoy_captain/devices_controller.rb +3 -6
  67. data/app/controllers/ahoy_captain/entry_pages_controller.rb +2 -4
  68. data/app/controllers/ahoy_captain/exit_pages_controller.rb +3 -4
  69. data/app/controllers/ahoy_captain/exports_controller.rb +14 -0
  70. data/app/controllers/ahoy_captain/filters/base_controller.rb +1 -3
  71. data/app/controllers/ahoy_captain/filters/goals_controller.rb +9 -0
  72. data/app/controllers/ahoy_captain/filters/pages/actions_controller.rb +1 -1
  73. data/app/controllers/ahoy_captain/filters/pages/entry_pages_controller.rb +3 -3
  74. data/app/controllers/ahoy_captain/filters/pages/exit_pages_controller.rb +2 -3
  75. data/app/controllers/ahoy_captain/filters/properties/names_controller.rb +11 -0
  76. data/app/controllers/ahoy_captain/filters/properties/values_controller.rb +15 -0
  77. data/app/controllers/ahoy_captain/filters/sources_controller.rb +1 -1
  78. data/app/controllers/ahoy_captain/filters/utms_controller.rb +1 -1
  79. data/app/controllers/ahoy_captain/properties_controller.rb +41 -0
  80. data/app/controllers/ahoy_captain/regions_controller.rb +3 -7
  81. data/app/controllers/ahoy_captain/sources_controller.rb +2 -5
  82. data/app/controllers/ahoy_captain/stats/base_controller.rb +86 -5
  83. data/app/controllers/ahoy_captain/stats/bounce_rates_controller.rb +2 -1
  84. data/app/controllers/ahoy_captain/stats/total_pageviews_controller.rb +2 -1
  85. data/app/controllers/ahoy_captain/stats/total_visits_controller.rb +2 -1
  86. data/app/controllers/ahoy_captain/stats/unique_visitors_controller.rb +3 -1
  87. data/app/controllers/ahoy_captain/stats/views_per_visits_controller.rb +3 -11
  88. data/app/controllers/ahoy_captain/stats/visit_durations_controller.rb +2 -1
  89. data/app/controllers/ahoy_captain/top_pages_controller.rb +2 -8
  90. data/app/decorators/ahoy_captain/application_decorator.rb +27 -3
  91. data/app/decorators/ahoy_captain/campaign_decorator.rb +8 -0
  92. data/app/decorators/ahoy_captain/city_decorator.rb +12 -0
  93. data/app/decorators/ahoy_captain/country_decorator.rb +10 -0
  94. data/app/decorators/ahoy_captain/device_decorator.rb +13 -2
  95. data/app/decorators/ahoy_captain/page_decorator.rb +11 -0
  96. data/app/decorators/ahoy_captain/region_decorator.rb +16 -0
  97. data/app/decorators/ahoy_captain/source_decorator.rb +7 -0
  98. data/app/helpers/ahoy_captain/application_helper.rb +62 -3
  99. data/app/models/ahoy_captain/comparison_mode.rb +72 -0
  100. data/app/models/ahoy_captain/export.rb +48 -0
  101. data/app/models/ahoy_captain/filter_parser.rb +82 -0
  102. data/app/models/ahoy_captain/range_from_params.rb +75 -0
  103. data/app/models/ahoy_captain/rangeable.rb +0 -3
  104. data/app/models/concerns/ahoy_captain/compare_mode.rb +19 -0
  105. data/app/models/concerns/ahoy_captain/limitable.rb +17 -0
  106. data/app/models/concerns/ahoy_captain/range_options.rb +1 -14
  107. data/app/presenters/ahoy_captain/dashboard_presenter.rb +18 -49
  108. data/app/presenters/ahoy_captain/goals_presenter.rb +3 -2
  109. data/app/queries/ahoy_captain/application_query.rb +78 -13
  110. data/app/queries/ahoy_captain/campaign_query.rb +14 -0
  111. data/app/queries/ahoy_captain/city_query.rb +11 -0
  112. data/app/queries/ahoy_captain/country_query.rb +10 -0
  113. data/app/queries/ahoy_captain/device_query.rb +10 -0
  114. data/app/queries/ahoy_captain/entry_pages_query.rb +3 -2
  115. data/app/queries/ahoy_captain/event_query.rb +20 -13
  116. data/app/queries/ahoy_captain/exit_pages_query.rb +6 -4
  117. data/app/queries/ahoy_captain/region_query.rb +11 -0
  118. data/app/queries/ahoy_captain/source_query.rb +10 -0
  119. data/app/queries/ahoy_captain/stats/average_views_per_visit_query.rb +11 -4
  120. data/app/queries/ahoy_captain/stats/average_visit_duration_query.rb +14 -2
  121. data/app/queries/ahoy_captain/stats/base_query.rb +18 -0
  122. data/app/queries/ahoy_captain/stats/bounce_rates_query.rb +15 -1
  123. data/app/queries/ahoy_captain/stats/total_pageviews_query.rb +2 -2
  124. data/app/queries/ahoy_captain/stats/total_visitors_query.rb +1 -1
  125. data/app/queries/ahoy_captain/stats/unique_visitors_query.rb +1 -1
  126. data/app/queries/ahoy_captain/stats/views_per_visit_query.rb +1 -1
  127. data/app/queries/ahoy_captain/stats/visit_duration_query.rb +3 -3
  128. data/app/queries/ahoy_captain/top_page_query.rb +13 -0
  129. data/app/queries/ahoy_captain/visit_query.rb +2 -3
  130. data/app/queries/concerns/ahoy_captain/comparable_queries.rb +25 -0
  131. data/app/queries/concerns/ahoy_captain/comparable_query.rb +138 -0
  132. data/app/queries/concerns/ahoy_captain/lazy_comparable_query.rb +42 -0
  133. data/app/views/ahoy_captain/devices/_table.html.erb +2 -0
  134. data/app/views/ahoy_captain/devices/index.html+details.erb +1 -1
  135. data/app/views/ahoy_captain/devices/index.html.erb +2 -2
  136. data/app/views/ahoy_captain/funnels/show.html.erb +5 -2
  137. data/app/views/ahoy_captain/goals/index.html.erb +2 -37
  138. data/app/views/ahoy_captain/layouts/application.html.erb +3 -4
  139. data/app/views/ahoy_captain/properties/_form.html.erb +6 -0
  140. data/app/views/ahoy_captain/properties/index.html.erb +3 -0
  141. data/app/views/ahoy_captain/properties/show.html.erb +6 -0
  142. data/app/views/ahoy_captain/realtimes/show.html.erb +1 -1
  143. data/app/views/ahoy_captain/roots/_filters.html.erb +80 -0
  144. data/app/views/ahoy_captain/roots/show.html.erb +76 -109
  145. data/app/views/ahoy_captain/stats/base/index.html.erb +34 -9
  146. data/app/views/ahoy_captain/stats/show.html.erb +8 -55
  147. data/config/routes.rb +9 -0
  148. data/lib/ahoy_captain/ahoy/event_methods.rb +35 -74
  149. data/lib/ahoy_captain/ahoy/visit_methods.rb +1 -1
  150. data/lib/ahoy_captain/configuration.rb +18 -7
  151. data/lib/ahoy_captain/engine.rb +22 -0
  152. data/lib/ahoy_captain/filter_configuration/filter.rb +16 -0
  153. data/lib/ahoy_captain/filter_configuration/filter_collection.rb +48 -0
  154. data/lib/ahoy_captain/filters_configuration.rb +77 -0
  155. data/lib/ahoy_captain/goals.rb +10 -2
  156. data/lib/ahoy_captain/period_collection.rb +1 -1
  157. data/lib/ahoy_captain/predicate_label.rb +7 -0
  158. data/lib/ahoy_captain/version.rb +1 -1
  159. data/lib/ahoy_captain.rb +7 -1
  160. data/lib/generators/ahoy_captain/templates/config.rb.tt +32 -0
  161. metadata +80 -21
  162. data/app/assets/javascript/ahoy_captain/controllers/filter_controller.js +0 -145
  163. data/app/assets/javascript/ahoy_captain/controllers/filter_tag_controller.js +0 -17
  164. data/app/assets/javascript/ahoy_captain/controllers/link_controller.js +0 -43
  165. data/app/assets/javascript/ahoy_captain/controllers/navigation_controller.js +0 -25
  166. data/app/models/ahoy_captain/current.rb +0 -9
  167. data/app/models/ahoy_captain/url_helpers.rb +0 -6
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 42069eea6d1d5b65765678fa1d352cf462048c09ad5bbe69715f0f147f0a3856
4
- data.tar.gz: cd0fab7a2a262b89f5688374cb566b482f0a9909b5358db5b02c9d3480f4ed21
3
+ metadata.gz: e860b8082ca3fad364eb34df632f017d81e63ff411c2fcae0501657aef5a578b
4
+ data.tar.gz: 4d0227c2eb7443e4d6a15f817d971f6176c43daad634d057de88f98f7ec8ae7b
5
5
  SHA512:
6
- metadata.gz: 464affe93d6d8ebfec8baf55f989a8402a6e265247259894c4f01a22b32047bc3cda4238c3e22ea06a7e6dae088353f23ed1a94cc65899ea24c7096b49a0d031
7
- data.tar.gz: 22b2352c2a7c9a21b8999292ee1bff64b87bc3306dd6ecb9f7e6f0c787306a3ec2132850c8954f15bac01e830f624a961bee868d7e3c8c5d3718d3b0b6946954
6
+ metadata.gz: 84a9922fec7838c6733263224f0b2a86e836b147fd8074e11428c47899ffd69317617b57f7dae503e59139d1c1ef134c46c2692ce298493830cd5b742a8fa780
7
+ data.tar.gz: 66e113dc9b86039b504d20153384d3fa55afe1f842a69da47d36aa03829fa0830b56a81411fd4c742626848973d1b89d43c4b523cd909ae8eb7bd6324f8261a5
data/README.md CHANGED
@@ -1,17 +1,12 @@
1
- # AhoyCaptain
1
+ # <img src="logo.png" style="max-height:100px" /> AhoyCaptain
2
2
 
3
- <img src="logo.png" style="max-width:100px" />
4
3
 
5
4
  A full-featured, mountable analytics dashboard for your Rails app, shamelessly inspired by Plausible Analytics, powered by the Ahoy gem.
6
5
 
7
6
  <a href="https://github.com/joshmn/ahoy_captain/blob/main/ss.jpg"><img src="ss.jpg" style="max-width:300px" /></a>
8
7
  ## Notice
9
8
 
10
- While this is fine to use in production, it was only built against a PostgreSQL instance. Some of the queries are certainly broken.
11
-
12
- ## Some assumptions
13
-
14
- Some hardcoded stuff as of writing; this will be more fully-featured in due time.
9
+ Currently requires using PG and a JSONB column for your data.
15
10
 
16
11
  ## Installation
17
12
 
@@ -29,11 +24,26 @@ $ bundle add ahoy_captain
29
24
  $ rails g ahoy_captain:install
30
25
  ```
31
26
 
32
- ### 3. Star this repo
27
+ ### 3. Make sure your events are setup correctly
28
+
29
+ AhoyCaptain doesn't do any tracking for you; it merely provides a dashboard for your data from the Ahoy gem.
30
+
31
+ By default, AhoyCaptain assumes you're tracking `controller` and `action` in your `Ahoy::Event` properties, and a page view event is named `$view`. See this section for more information: https://github.com/ankane/ahoy#events
32
+
33
+ For a quick sanity check:
34
+
35
+ ```ruby
36
+ AhoyCaptain.event.where(name: AhoyCaptain.config.event[:view_name]).count
37
+ AhoyCaptain.event.with_routes.count
38
+ ```
39
+
40
+ This can be fully-customized. See the initializer `config/initializers/ahoy_captain.rb` for more.
41
+
42
+ ### 4. Star this repo
33
43
 
34
44
  No, seriously, I need all the internet clout I can get.
35
45
 
36
- ### 4. Analyze your nightmares
46
+ ### 5. Analyze your nightmares
37
47
 
38
48
  If you have a large dataset (> 1GB) you probably want some indexes. `rails g ahoy_captain:migration`
39
49
 
@@ -52,12 +62,14 @@ If you have a large dataset (> 1GB) you probably want some indexes. `rails g aho
52
62
  * Device type
53
63
  * OS
54
64
  * UTM tags
65
+ * Goal
66
+ * Event Property
67
+ * CSV exports
68
+ * Date comparison
55
69
 
56
70
  ## Coming soon ™️
57
71
 
58
- * Date comparison
59
- * More filters
60
- * CSV exports
72
+ * Bug fixes and performance improvements
61
73
 
62
74
  ## Contributors
63
75
 
@@ -65,7 +77,7 @@ This was built during the Rails Hackathon in July 2023 with [afogel](https://git
65
77
 
66
78
  ## Contributions
67
79
 
68
- Please and thank you in advance!
80
+ Do your worst; please and thank you in advance! :)
69
81
 
70
82
  ## License
71
83
 
@@ -1,4 +1,4 @@
1
- import "@hotwired/turbo-rails"
2
- import "controllers"
3
- import "chartkick"
4
- import "Chart.bundle"
1
+ import '@hotwired/turbo-rails';
2
+ import 'controllers';
3
+ import 'chartkick';
4
+ import 'Chart.bundle';
@@ -0,0 +1,30 @@
1
+ import { Controller } from '@hotwired/stimulus';
2
+
3
+ export default class extends Controller {
4
+ static targets = ["link", "alt"]
5
+ static values = {
6
+ classes: { type: Array, default: ["text-primary", "font-semibold"] }
7
+ }
8
+
9
+ // sometimes the target is not the link itself but a child element, and we want to highlight something other than the
10
+ // link itself.
11
+ // this can be bettered
12
+ connect() {
13
+ this.handleLinkClick = (event) => {
14
+ let link = null;
15
+ if(event.target.tagName === "A") {
16
+ link = event.target;
17
+ } else {
18
+ link = (event.target.closest('a').querySelector('[data-active-links-target="link"]'))
19
+ }
20
+ this.linkTargets.forEach(link => this.classesValue.forEach(klass => link.classList.remove(klass)))
21
+ this.classesValue.forEach(klass => link.classList.add(klass))
22
+ }
23
+ this.linkTargets.forEach(link => {
24
+ link.addEventListener('click', this.handleLinkClick)
25
+ })
26
+ this.altTargets.forEach(target => {
27
+ target.addEventListener('click', this.handleLinkClick)
28
+ })
29
+ }
30
+ }
@@ -1,9 +1,9 @@
1
- import { Application } from "@hotwired/stimulus"
1
+ import { Application } from '@hotwired/stimulus';
2
2
 
3
- const application = Application.start()
3
+ const application = Application.start();
4
4
 
5
5
  // Configure Stimulus development experience
6
- application.debug = false
7
- window.Stimulus = application
6
+ application.debug = false;
7
+ window.Stimulus = application;
8
8
 
9
- export { application }
9
+ export { application };
@@ -1,14 +1,21 @@
1
- import {Controller} from "@hotwired/stimulus"
1
+ import { Controller } from '@hotwired/stimulus';
2
2
 
3
3
  export default class extends Controller {
4
- connect() {
5
- if(new URLSearchParams(window.location.search).get("period") === 'realtime') {
6
- this.element.querySelectorAll('turbo-frame').forEach(frame => {
7
- setInterval(() => {
8
- frame.reload()
9
- }, 1000 * 30);
10
- })
11
- }
4
+ connect() {
5
+ window.comboboxConnected = 0;
6
+ if (new URLSearchParams(window.location.search).get('period') === 'realtime') {
7
+ this.element.querySelectorAll('turbo-frame').forEach((frame) => {
8
+ setInterval(() => {
9
+ frame.reload();
10
+ }, 1000 * 30);
11
+ });
12
12
  }
13
+ }
14
+
15
+ comboboxInit(event) {
16
+ if(event.detail.combobox.selectTarget.id === "property-name" || event.detail.combobox.selectTarget.id === "property-value") {
17
+ window.comboboxConnected += 1;
18
+ }
19
+ }
13
20
 
14
21
  }
@@ -0,0 +1,341 @@
1
+ import { Controller } from "@hotwired/stimulus";
2
+ import "classnames"
3
+
4
+ const debounce = (func, delay) => {
5
+ let debounceTimer
6
+ return function() {
7
+ const context = this
8
+ const args = arguments
9
+ clearTimeout(debounceTimer)
10
+ debounceTimer = setTimeout(() => func.apply(context, args), delay)
11
+ }
12
+ }
13
+
14
+ export default class extends Controller {
15
+ static targets = ["input", "list", "option", "container", "select", "highlighted", "box", "selected"];
16
+ static classes = ["boxOpen"]
17
+ static values = {
18
+ options: Array,
19
+ isLoading: Boolean,
20
+ isOpen: Boolean,
21
+ disabled: Boolean,
22
+ input: String,
23
+ highlightedIndex: Number,
24
+ singleOption: Boolean,
25
+ freeChoice: Boolean,
26
+ selected: Array,
27
+ url: String,
28
+ query: String
29
+ };
30
+
31
+ connect() {
32
+ this.isLoadingValue = false;
33
+ this.isOpenValue = false;
34
+ this.inputValue = '';
35
+ this.highlightedIndexValue = 0;
36
+ this.clickHandler = this.clickHandler.bind(this)
37
+
38
+ this.inputTarget.addEventListener('keydown', this.onKeyDown.bind(this))
39
+ this.debouncedFetchOptions = debounce(this.fetchOptions.bind(this), 250);
40
+ this.checkDisabledState();
41
+ if(this.singleOptionValue) {
42
+ this.selectTarget.removeAttribute('multiple')
43
+ } else {
44
+ this.selectTarget.multiple = true
45
+ }
46
+
47
+ Object.defineProperty(this.selectTarget, "combobox", {
48
+ enumerable: false,
49
+ configurable: false,
50
+ writable: false,
51
+ value: this,
52
+ });
53
+
54
+ window.dispatchEvent(new CustomEvent('combobox:init', { detail: { combobox: this } }))
55
+ this.search = new URLSearchParams(window.location.search);
56
+ this.search.delete(this.selectTarget.name)
57
+ }
58
+
59
+ checkDisabledState() {
60
+ if (this.disabledValue) {
61
+ this.element.classList.add('opacity-80', 'cursor-default', 'pointer-events-none');
62
+ } else {
63
+ this.element.classList.remove('opacity-80', 'cursor-default', 'pointer-events-none')
64
+ }
65
+ }
66
+
67
+ onInput(event) {
68
+ this.inputValue = event.target.value;
69
+ this.debouncedFetchOptions(this.inputValue);
70
+ }
71
+
72
+ fetchOptions(query) {
73
+ if(this.disabledValue) { return }
74
+ this.isLoadingValue = true;
75
+ this.isOpenValue = true;
76
+
77
+ const searchParams = new URLSearchParams(this.search.toString());
78
+ const formData = new FormData(this.element.form);
79
+
80
+ let deleted = [];
81
+ for (const [key, value] of formData) {
82
+ if(!deleted.includes(key)) {
83
+ searchParams.delete(key)
84
+ deleted.push(key)
85
+ }
86
+ }
87
+
88
+ searchParams.delete(this.element.name);
89
+ searchParams.delete(this.queryValue);
90
+ searchParams.set(this.queryValue, query);
91
+
92
+ fetch(`${this.urlValue}?${searchParams.toString()}`).then(resp => resp.json()).then(loadedOptions => {
93
+ this.isLoadingValue = false;
94
+ this.highlightedIndexValue = 0;
95
+ this.optionsValue = loadedOptions.map(option => ({ text: option.text, value: option.text }));
96
+ });
97
+ }
98
+
99
+ highlight(element) {
100
+ const index = parseInt(element.target.dataset.index);
101
+ this.highlightIndex(index)
102
+ }
103
+
104
+ scrollToOption(index) {
105
+ const optionElement = this.listTarget.querySelector(`[data-index="${index}"]`);
106
+ if (optionElement) {
107
+ optionElement.scrollIntoView({ block: 'center' });
108
+ }
109
+ }
110
+
111
+ highlightIndex(index) {
112
+ this.highlightedIndexValue = index;
113
+ this.scrollToOption(index);
114
+ }
115
+
116
+ setSelected(values) {
117
+ this.selectedValue = values;
118
+ }
119
+
120
+ setDisabled(value) {
121
+ this.disabledValue = value
122
+ }
123
+
124
+ onKeyDown(event) {
125
+ switch (event.key) {
126
+ case 'Enter':
127
+ if (!this.isOpenValue || this.isLoadingValue || this.optionTargets.length === 0) return;
128
+ const option = this.listTarget.querySelector(`[data-index="${this.highlightedIndexValue}"]`);
129
+ if(option) {
130
+ this.selectOption(option);
131
+ }
132
+
133
+ event.preventDefault();
134
+ break;
135
+ case 'Escape':
136
+ if (!this.isOpenValue || this.isLoadingValue) return;
137
+ this.isOpenValue = false;
138
+ this.inputTarget.focus();
139
+ event.preventDefault();
140
+ break;
141
+ case 'ArrowDown':
142
+ if(this.isOpenValue) {
143
+ this.highlightIndex(this.highlightedIndexValue + 1)
144
+ } else {
145
+ this.isOpenValue = true
146
+ }
147
+ break;
148
+ case 'ArrowUp':
149
+ if(this.isOpenValue) {
150
+ this.highlightIndex(this.highlightedIndexValue - 1)
151
+ } else {
152
+ this.isOpenValue = true
153
+ }
154
+ break;
155
+ }
156
+ }
157
+
158
+ selectOption(selected) {
159
+ let value = null;
160
+ if(selected.tagName) {
161
+ value = selected.dataset.value;
162
+ if(value === undefined) {
163
+ value = selected.parentElement.dataset.value
164
+ }
165
+ } else {
166
+ value = selected.target.dataset.value;
167
+ if(value === undefined) {
168
+ value = selected.target.parentElement.dataset.value
169
+ }
170
+ }
171
+
172
+ const option = this.optionsValue.filter(option => option.value === value)[0];
173
+ if(this.singleOptionValue) {
174
+ this.selectedValue = [option]
175
+ } else {
176
+ this.selectedValue = [...this.selectedValue, option]
177
+ }
178
+ this.isOpenValue = false;
179
+ this.inputTarget.value = '';
180
+ this.highlightedIndexValue = 0
181
+ }
182
+
183
+ toggleOpen() {
184
+ if (!this.isOpenValue) {
185
+ this.debouncedFetchOptions(this.inputValue);
186
+ this.inputTarget.focus();
187
+ document.addEventListener('click', this.clickHandler)
188
+ } else {
189
+ this.inputValue = '';
190
+ this.isOpenValue = false;
191
+ document.removeEventListener('click', this.clickHandler)
192
+ }
193
+ }
194
+
195
+ clickHandler(event) {
196
+ if(event.target.classList.contains('combobox-option')) {
197
+ return
198
+ } else {
199
+ this.toggleOpen()
200
+ return
201
+ }
202
+ }
203
+
204
+ isOpenValueChanged(current) {
205
+ if(current) {
206
+ this.boxTarget.classList.add(...this.boxOpenClasses)
207
+ } else {
208
+ this.boxTarget.classList.remove(...this.boxOpenClasses)
209
+
210
+ }
211
+ this.listTarget.style.display = current ? 'block' : 'none';
212
+ }
213
+
214
+ highlightedIndexValueChanged(current, previous) {
215
+ const prev = this.listTarget.querySelector(`[data-index="${previous}"]`)
216
+ if(prev) {
217
+ prev.classList.remove('bg-primary-600', 'text-white')
218
+ }
219
+ const now = this.listTarget.querySelector(`[data-index="${current}"]`);
220
+ if(now) {
221
+ now.classList.add('bg-primary-600', 'text-white')
222
+ }
223
+ }
224
+
225
+ renderDropDownContent() {
226
+ this.listTarget.innerHTML = "";
227
+
228
+ const visibleOptions = this.visibleOptions()
229
+ const matchesFound = visibleOptions.length > 0 && visibleOptions.some(option => !this.isOptionDisabled(option))
230
+
231
+ if (matchesFound) {
232
+ return this.renderOptions(visibleOptions.filter(option => !this.isOptionDisabled(option)))
233
+ }
234
+
235
+ if(this.isLoadingValue) {
236
+ this.listTarget.innerHTML = `<div>Is Loading..</div>`
237
+ return
238
+ }
239
+
240
+ if(this.freeChoiceValue) {
241
+ this.listTarget.innerHTML = `<div class="relative cursor-default select-none py-2 px-4 text-gray-700 dark:text-gray-300">Start typing to apply filter</div>`
242
+ return
243
+ }
244
+
245
+ this.listTarget.innerHTML = `<div class="relative cursor-default select-none py-2 px-4 text-gray-700 dark:text-gray-300">
246
+ No matches found in the current dashboard. Try selecting a different time range or searching for something different.
247
+ </div>`
248
+
249
+ }
250
+ renderOptions(options) {
251
+ options.forEach((option, index) => {
252
+ const optionElement = document.createElement("li");
253
+ const isHighlighted = this.highlightedIndexValue === index;
254
+ optionElement.innerHTML = `<span class="block truncate" data-index="${index}">${option.text}</span>`;
255
+ optionElement.className = classNames('combobox-option relative cursor-pointer select-none py-2 px-3 hover:bg-primary-600 hover:text-white', {
256
+ 'text-accent-900': !isHighlighted,
257
+ 'bg-primary-600 text-white': isHighlighted,
258
+ });
259
+
260
+ if(isHighlighted) {
261
+ optionElement.dataset.comboboxTarget = "option"
262
+ }
263
+ optionElement.dataset.action = "click->combobox#selectOption"
264
+ optionElement.dataset.index = index;
265
+ optionElement.dataset.value = option.value
266
+ optionElement.id = `combobox-option-${index}`;
267
+
268
+ this.listTarget.appendChild(optionElement);
269
+ });
270
+ }
271
+ optionsValueChanged(current, before) {
272
+ this.renderDropDownContent()
273
+ }
274
+
275
+ isOptionDisabled(option) {
276
+ const disabled = this.selectedValue.some((val) => val.value === option.value)
277
+
278
+ return disabled
279
+ }
280
+
281
+ visibleOptions() {
282
+ const visibleOptions = [...this.optionsValue]
283
+ if (this.freeChoiceValue && this.inputTarget.length > 0 && this.optionsValue.every(option => option.value !== this.inputTarget.value)) {
284
+ visibleOptions.push({value: this.inputTarget.value, label: this.inputTarget.value, freeChoice: true})
285
+ }
286
+
287
+ return visibleOptions
288
+ }
289
+
290
+ selectedValueChanged(current, prev) {
291
+ this.renderSelectedValues()
292
+ this.renderDropDownContent()
293
+ }
294
+
295
+ removeOption(e) {
296
+ e.stopPropagation()
297
+ const option = this.selectTarget.querySelector(`option[value="${e.target.dataset.value}"]`);
298
+ option.remove()
299
+ const newValues = [];
300
+ this.selectTarget.querySelectorAll('option[selected]').forEach(option => {
301
+ newValues.push({text: option.text, value: option.value })
302
+ })
303
+ this.selectedValue = newValues;
304
+ this.isOpenValue = false
305
+ }
306
+
307
+ renderSelectedValues() {
308
+ this.selectTarget.innerHTML = ""
309
+ this.selectedTarget.innerHTML = ""
310
+ this.selectedValue.forEach(value => {
311
+ const option = document.createElement('option');
312
+ option.text = value.text;
313
+ option.value = value.value;
314
+ option.setAttribute('selected', 'selected')
315
+ this.selectTarget.appendChild(option)
316
+
317
+ const el = document.createElement("div");
318
+ el.classList.add('text-primary-content', 'bg-primary', 'flex', 'justify-between', 'w-full', 'rounded-sm', 'px-2', 'py-0.5', 'm-0.5', 'text-sm');
319
+ el.innerHTML = `<span class="break-all">${option.text}</span><span class="cursor-pointer font-bold ml-1" data-action="click->combobox#removeOption" data-value="${option.value}" >×</span>`;
320
+ this.selectedTarget.appendChild(el)
321
+ })
322
+ var event = new Event('change');
323
+ this.selectTarget.dispatchEvent(event);
324
+ if(this.selectedValue.length === 0) {
325
+ this.selectedTarget.style.display = "none"
326
+ } else {
327
+ this.selectedTarget.style.display = ""
328
+ }
329
+ }
330
+
331
+ disabledValueChanged(current) {
332
+ if(current) {
333
+ this.isOpenValue = false
334
+ this.inputTarget.disabled = true
335
+ this.checkDisabledState()
336
+ } else {
337
+ this.inputTarget.removeAttribute('disabled')
338
+ this.checkDisabledState()
339
+ }
340
+ }
341
+ }
@@ -1,15 +1,15 @@
1
- import {Controller} from "@hotwired/stimulus"
1
+ import { Controller } from '@hotwired/stimulus';
2
2
 
3
3
  export default class extends Controller {
4
4
  static values = {
5
- target: String
6
- }
7
-
5
+ target: String,
6
+ };
7
+
8
8
  connect() {
9
9
  this.modal = document.querySelector('#detailsModal');
10
10
  this.turboFrame = document.querySelector('#detailsModal turbo-frame');
11
11
  }
12
-
12
+
13
13
  openModal(e) {
14
14
  e.preventDefault();
15
15
  this.modal.showModal();
@@ -1,7 +1,7 @@
1
- import {Controller} from "@hotwired/stimulus"
1
+ import { Controller } from '@hotwired/stimulus';
2
2
 
3
3
  export default class extends Controller {
4
- static targets = ["label", "close"]
4
+ static targets = ['label', 'close'];
5
5
 
6
6
  setLabel(event) {
7
7
  this.labelTarget.innerText = event.target.innerText;
@@ -0,0 +1,12 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ // Connects to data-controller="filter--item"
4
+ export default class extends Controller {
5
+ static values = {
6
+ modal: String
7
+ };
8
+
9
+ openModal() {
10
+ document.getElementById(this.modalValue).showModal()
11
+ }
12
+ }
@@ -0,0 +1,13 @@
1
+ import {Controller} from "@hotwired/stimulus"
2
+
3
+ export default class extends Controller {
4
+ handleReset(event) {
5
+ event.preventDefault();
6
+ const openModal = document.querySelector('dialog.modal[open]');
7
+ openModal.querySelectorAll('input, select').forEach(element => {
8
+ element.value = ""
9
+ });
10
+ openModal.close()
11
+ this.element.requestSubmit()
12
+ }
13
+ }
@@ -0,0 +1,45 @@
1
+ import { Controller } from '@hotwired/stimulus';
2
+
3
+ export default class extends Controller {
4
+
5
+ // reverts the modal back to its original state if it was simply closed
6
+ connect() {
7
+ const targetNode = this.element;
8
+ const config = { attributes: true, childList: false, subtree: false };
9
+ const callback = (mutationList, observer) => {
10
+ for (const mutation of mutationList) {
11
+ if(mutation.attributeName === "open") {
12
+ if(this.element.open) {
13
+ if(!this.originalValues) {
14
+ this.originalValues = {};
15
+ const formElements = this.element.querySelectorAll('select');
16
+ formElements.forEach(el => {
17
+ if(el.combobox) {
18
+ this.originalValues[el.id] = el.combobox.selectedValue;
19
+ } else {
20
+ this.originalValues[el.id] = el.value;
21
+ }
22
+ })
23
+ }
24
+ } else {
25
+ const formElements = this.element.querySelectorAll('select');
26
+ formElements.forEach(el => {
27
+ if(this.originalValues[el.id]) {
28
+ if(el.combobox) {
29
+ el.combobox.setSelected(this.originalValues[el.id])
30
+ } else {
31
+ el.value = this.originalValues[el.id]
32
+ }
33
+ }
34
+ })
35
+ }
36
+ }
37
+ }
38
+ };
39
+
40
+ const observer = new MutationObserver(callback);
41
+ observer.observe(targetNode, config);
42
+ }
43
+
44
+
45
+ }