ahoy_captain 0.77 → 0.82

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (110) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/javascript/ahoy_captain/application.js +4 -4
  3. data/app/assets/javascript/ahoy_captain/controllers/active_links_controller.js +15 -0
  4. data/app/assets/javascript/ahoy_captain/controllers/application.js +5 -5
  5. data/app/assets/javascript/ahoy_captain/controllers/application_controller.js +9 -10
  6. data/app/assets/javascript/ahoy_captain/controllers/details_modal_controller.js +5 -5
  7. data/app/assets/javascript/ahoy_captain/controllers/dropdown_label_controller.js +2 -2
  8. data/app/assets/javascript/ahoy_captain/controllers/filter_form_controller.js +13 -0
  9. data/app/assets/javascript/ahoy_captain/controllers/filter_tag_controller.js +11 -8
  10. data/app/assets/javascript/ahoy_captain/controllers/funnel_chart_controller.js +77 -133
  11. data/app/assets/javascript/ahoy_captain/controllers/index.js +4 -3
  12. data/app/assets/javascript/ahoy_captain/controllers/interval_controller.js +10 -0
  13. data/app/assets/javascript/ahoy_captain/controllers/line_chart_controller.js +37 -0
  14. data/app/assets/javascript/ahoy_captain/controllers/predicate_select_controller.js +10 -0
  15. data/app/assets/javascript/ahoy_captain/controllers/realtime_controller.js +9 -8
  16. data/app/assets/javascript/ahoy_captain/controllers/search_select_controller.js +65 -0
  17. data/app/components/ahoy_captain/filter/modal_component.html.erb +6 -5
  18. data/app/components/ahoy_captain/filter/select_component.html.erb +13 -11
  19. data/app/components/ahoy_captain/filter/select_component.rb +21 -4
  20. data/app/components/ahoy_captain/table_component.html.erb +2 -35
  21. data/app/components/ahoy_captain/table_component.rb +13 -5
  22. data/app/components/ahoy_captain/tables/headers/devices_header_component.html.erb +3 -0
  23. data/app/components/ahoy_captain/tables/headers/devices_header_component.rb +9 -0
  24. data/app/components/ahoy_captain/tables/headers/goals_header_component.html.erb +6 -0
  25. data/app/components/ahoy_captain/tables/headers/goals_header_component.rb +9 -0
  26. data/app/components/ahoy_captain/tables/headers/header_component.html.erb +5 -0
  27. data/app/components/ahoy_captain/tables/headers/header_component.rb +12 -0
  28. data/app/components/ahoy_captain/tables/rows/devices_row_component.html.erb +5 -0
  29. data/app/components/ahoy_captain/tables/rows/devices_row_component.rb +12 -0
  30. data/app/components/ahoy_captain/tables/rows/goals_row_component.html.erb +11 -0
  31. data/app/components/ahoy_captain/tables/rows/goals_row_component.rb +12 -0
  32. data/app/components/ahoy_captain/tables/rows/row_component.html.erb +6 -0
  33. data/app/components/ahoy_captain/tables/rows/row_component.rb +41 -0
  34. data/app/controllers/ahoy_captain/application_controller.rb +17 -15
  35. data/app/controllers/ahoy_captain/campaigns_controller.rb +2 -10
  36. data/app/controllers/ahoy_captain/cities_controller.rb +2 -6
  37. data/app/controllers/ahoy_captain/countries_controller.rb +2 -6
  38. data/app/controllers/ahoy_captain/devices_controller.rb +3 -6
  39. data/app/controllers/ahoy_captain/entry_pages_controller.rb +2 -4
  40. data/app/controllers/ahoy_captain/exit_pages_controller.rb +3 -4
  41. data/app/controllers/ahoy_captain/exports_controller.rb +15 -0
  42. data/app/controllers/ahoy_captain/filters/pages/entry_pages_controller.rb +2 -2
  43. data/app/controllers/ahoy_captain/filters/pages/exit_pages_controller.rb +1 -2
  44. data/app/controllers/ahoy_captain/filters/properties/names_controller.rb +11 -0
  45. data/app/controllers/ahoy_captain/filters/properties/values_controller.rb +15 -0
  46. data/app/controllers/ahoy_captain/realtimes_controller.rb +1 -1
  47. data/app/controllers/ahoy_captain/regions_controller.rb +3 -7
  48. data/app/controllers/ahoy_captain/sources_controller.rb +2 -5
  49. data/app/controllers/ahoy_captain/stats/base_controller.rb +61 -0
  50. data/app/controllers/ahoy_captain/stats/bounce_rates_controller.rb +4 -2
  51. data/app/controllers/ahoy_captain/stats/total_pageviews_controller.rb +2 -1
  52. data/app/controllers/ahoy_captain/stats/total_visits_controller.rb +2 -1
  53. data/app/controllers/ahoy_captain/stats/unique_visitors_controller.rb +2 -1
  54. data/app/controllers/ahoy_captain/stats/views_per_visits_controller.rb +10 -6
  55. data/app/controllers/ahoy_captain/stats/visit_durations_controller.rb +2 -1
  56. data/app/controllers/ahoy_captain/top_pages_controller.rb +2 -8
  57. data/app/decorators/ahoy_captain/application_decorator.rb +27 -3
  58. data/app/decorators/ahoy_captain/campaign_decorator.rb +8 -0
  59. data/app/decorators/ahoy_captain/city_decorator.rb +12 -0
  60. data/app/decorators/ahoy_captain/country_decorator.rb +10 -0
  61. data/app/decorators/ahoy_captain/device_decorator.rb +13 -2
  62. data/app/decorators/ahoy_captain/page_decorator.rb +11 -0
  63. data/app/decorators/ahoy_captain/region_decorator.rb +16 -0
  64. data/app/decorators/ahoy_captain/source_decorator.rb +7 -0
  65. data/app/models/ahoy_captain/export.rb +48 -0
  66. data/app/presenters/ahoy_captain/dashboard_presenter.rb +24 -16
  67. data/app/presenters/ahoy_captain/funnel_presenter.rb +32 -29
  68. data/app/presenters/ahoy_captain/goals_presenter.rb +32 -23
  69. data/app/queries/ahoy_captain/application_query.rb +9 -5
  70. data/app/queries/ahoy_captain/campaign_query.rb +14 -0
  71. data/app/queries/ahoy_captain/city_query.rb +11 -0
  72. data/app/queries/ahoy_captain/country_query.rb +10 -0
  73. data/app/queries/ahoy_captain/device_query.rb +10 -0
  74. data/app/queries/ahoy_captain/entry_pages_query.rb +3 -2
  75. data/app/queries/ahoy_captain/event_query.rb +26 -9
  76. data/app/queries/ahoy_captain/exit_pages_query.rb +6 -4
  77. data/app/queries/ahoy_captain/region_query.rb +11 -0
  78. data/app/queries/ahoy_captain/source_query.rb +10 -0
  79. data/app/queries/ahoy_captain/stats/average_visit_duration_query.rb +3 -7
  80. data/app/queries/ahoy_captain/stats/bounce_rates_query.rb +9 -6
  81. data/app/queries/ahoy_captain/stats/total_visitors_query.rb +1 -1
  82. data/app/queries/ahoy_captain/stats/unique_visitors_query.rb +1 -1
  83. data/app/queries/ahoy_captain/stats/views_per_visit_query.rb +2 -2
  84. data/app/queries/ahoy_captain/stats/visit_duration_query.rb +4 -4
  85. data/app/queries/ahoy_captain/top_page_query.rb +13 -0
  86. data/app/queries/ahoy_captain/visit_query.rb +12 -12
  87. data/app/views/ahoy_captain/devices/_table.html.erb +5 -0
  88. data/app/views/ahoy_captain/devices/index.html+details.erb +1 -1
  89. data/app/views/ahoy_captain/devices/index.html.erb +2 -2
  90. data/app/views/ahoy_captain/goals/index.html.erb +3 -35
  91. data/app/views/ahoy_captain/layouts/application.html.erb +1 -1
  92. data/app/views/ahoy_captain/roots/show.html.erb +81 -73
  93. data/app/views/ahoy_captain/stats/base/index.html.erb +8 -1
  94. data/app/views/ahoy_captain/stats/show.html.erb +1 -1
  95. data/config/routes.rb +7 -0
  96. data/lib/ahoy_captain/ahoy/event_methods.rb +29 -75
  97. data/lib/ahoy_captain/configuration.rb +2 -2
  98. data/lib/ahoy_captain/engine.rb +1 -0
  99. data/lib/ahoy_captain/goals.rb +15 -3
  100. data/lib/ahoy_captain/period_collection.rb +1 -1
  101. data/lib/ahoy_captain/version.rb +1 -1
  102. data/lib/generators/ahoy_captain/migration_generator.rb +21 -0
  103. data/lib/generators/ahoy_captain/templates/config.rb.tt +18 -3
  104. data/lib/generators/ahoy_captain/templates/migration.rb.tt +7 -0
  105. metadata +48 -7
  106. data/app/assets/javascript/ahoy_captain/controllers/filter_controller.js +0 -145
  107. data/app/assets/javascript/ahoy_captain/controllers/link_controller.js +0 -43
  108. data/app/assets/javascript/ahoy_captain/controllers/navigation_controller.js +0 -25
  109. data/app/models/ahoy_captain/current.rb +0 -9
  110. 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: bc2218d617b8530eb24d10b8f8cf8da63364fece7069a6610fb621c80eda9450
4
- data.tar.gz: 6a9fd5388653ad8654062447fa564c81fc716c644d36cf00c64987c6d2627f3c
3
+ metadata.gz: c46c8a68d18711be0ceee5db679399c9945cd8735666cb44a5577e46e2371294
4
+ data.tar.gz: 28e7a6863f0b7b7d3c32298b08198f9fcbcf06ea6e2350d6aa998250f836fd80
5
5
  SHA512:
6
- metadata.gz: 07c8677c2052fc124aa9e23338fcd75d8b963bf18d27a1f804e1e506e21bcc96e74803c0ae1cc051cae420fe572dd2144c1f2275e89ea20dba818e850a665da4
7
- data.tar.gz: e8657c6e04703d68d105912a6bdf75335aa98eb38311c76214828d1cf5e35f351c368584fd39cd5c61ef2215adf8dfffae42afff763590cd1021d6d58522846d
6
+ metadata.gz: 8a238b7d58c0fe3c39ced96f0e3d33233d06fe78d4c83ede1a14d45ae07045cb9aeb8d22bd004ddf86a8e63eb657b938c9e90cf0b10f092c7677a34dffb2b207
7
+ data.tar.gz: 5bf02dd5c2de5db42618bc133a9b54e6cacb6602a9305dfdb1423a906a8f92a704ccc996ea63adfb6e30dbd1de70d96a518a88d9a7fbe9a7a27350f104f87ad8
@@ -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,15 @@
1
+ import { Controller } from '@hotwired/stimulus';
2
+
3
+ export default class extends Controller {
4
+ static targets = ["link"]
5
+ connect() {
6
+ this.handleLinkClick = (event) => {
7
+ this.linkTargets.forEach(link => link.classList.remove('text-primary', 'font-semibold'))
8
+ event.target.classList.add('text-primary', 'font-semibold')
9
+ }
10
+ this.linkTargets.forEach(link => {
11
+ link.addEventListener('click', this.handleLinkClick)
12
+ })
13
+ }
14
+
15
+ }
@@ -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,13 @@
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
+ 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
+ });
12
11
  }
13
-
12
+ }
14
13
  }
@@ -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,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
+ }
@@ -1,17 +1,20 @@
1
- import {Controller} from "@hotwired/stimulus"
1
+ import { Controller } from '@hotwired/stimulus';
2
2
 
3
3
  export default class extends Controller {
4
- static targets = ["delete"];
4
+ static targets = ['delete'];
5
+
5
6
  static values = {
6
7
  columnPredicate: String,
7
- category: String
8
- }
8
+ category: String,
9
+ };
9
10
 
10
11
  remove(event) {
11
12
  event.preventDefault();
12
- this.dispatch('remove', {detail: {
13
- paramKey: `q[${this.columnPredicateValue}]`,
14
- paramValue: this.categoryValue
15
- }})
13
+ this.dispatch('remove', {
14
+ detail: {
15
+ paramKey: `q[${this.columnPredicateValue}]`,
16
+ paramValue: this.categoryValue,
17
+ },
18
+ });
16
19
  }
17
20
  }
@@ -1,143 +1,87 @@
1
- import {Controller} from "@hotwired/stimulus"
1
+ import { Controller } from '@hotwired/stimulus';
2
2
  import 'chartjs-plugin-datalabels';
3
- const THOUSAND = 1000
4
- const HUNDRED_THOUSAND = 100000
5
- const MILLION = 1000000
6
- const HUNDRED_MILLION = 100000000
7
- const BILLION = 1000000000
8
- const HUNDRED_BILLION = 100000000000
9
- const TRILLION = 1000000000000
10
-
11
- function numberFormatter(num) {
12
- if (num >= THOUSAND && num < MILLION) {
13
- const thousands = num / THOUSAND
14
- if (thousands === Math.floor(thousands) || num >= HUNDRED_THOUSAND) {
15
- return Math.floor(thousands) + 'k'
16
- } else {
17
- return (Math.floor(thousands * 10) / 10) + 'k'
18
- }
19
- } else if (num >= MILLION && num < BILLION) {
20
- const millions = num / MILLION
21
- if (millions === Math.floor(millions) || num >= HUNDRED_MILLION) {
22
- return Math.floor(millions) + 'M'
23
- } else {
24
- return (Math.floor(millions * 10) / 10) + 'M'
25
- }
26
- } else if (num >= BILLION && num < TRILLION) {
27
- const billions = num / BILLION
28
- if (billions === Math.floor(billions) || num >= HUNDRED_BILLION) {
29
- return Math.floor(billions) + 'B'
30
- } else {
31
- return (Math.floor(billions * 10) / 10) + 'B'
32
- }
33
- } else {
34
- return num
35
- }
36
- }
37
3
 
38
4
  export default class extends Controller {
39
- connect() {
40
- const funnel = JSON.parse(this.element.dataset.data);
41
-
42
- const getPalette = () => {
43
- return {
44
- dataLabelBackground: 'rgba(25, 30, 56, 0.97)',
45
- dataLabelTextColor: 'rgb(243, 244, 246)',
46
- visitorsBackground: 'rgb(99, 102, 241)',
47
- dropoffBackground: '#2F3949',
48
- dropoffStripes: 'rgb(25, 30, 56)',
49
- stepNameLegendColor: 'rgb(228, 228, 231)',
50
- visitorsLegendClass: 'bg-indigo-500',
51
- dropoffLegendClass: 'bg-gray-600',
52
- smallBarClass: 'bg-indigo-500'
53
- }
54
- }
55
-
5
+ connect() {
6
+ const funnel = JSON.parse(this.element.dataset.data);
56
7
 
8
+ const fontFamily = 'ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"';
9
+ const labels = funnel.steps.map((step) => step.name);
10
+ const stepData = funnel.steps.map((step) => step.total_events);
11
+ const dropOffData = funnel.steps.map((step) => step.drop_off * 100);
57
12
 
58
- var fontFamily = 'ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"'
59
- const calcBarThickness = (ctx) => {
60
- if (ctx.dataset.data.length <= 3) {
61
- return 160
62
- } else {
63
- return Math.floor(650 / ctx.dataset.data.length)
64
- }
65
- }
13
+ const data = {
14
+ labels,
15
+ datasets: [
16
+ {
17
+ label: 'Visitors',
18
+ data: stepData,
19
+ borderRadius: 4,
20
+ stack: 'Stack 0',
21
+ },
22
+ {
23
+ label: 'Dropoff',
24
+ data: dropOffData,
25
+ borderRadius: 4,
26
+ stack: 'Stack 0',
27
+ },
28
+ ],
29
+ };
66
30
 
67
- const labels = funnel.steps.map((step) => step.step)
68
- const stepData = funnel.steps.map((step) => step.count)
69
- const dropOffData = funnel.steps.map((step) => step.drop_off)
70
-
71
- const data = {
72
- labels: labels,
73
- datasets: [
74
- {
75
- label: 'Visitors',
76
- data: stepData,
77
- borderRadius: 4,
78
- stack: 'Stack 0',
79
- },
80
- {
81
- label: 'Dropoff',
82
- data: dropOffData,
83
- borderRadius: 4,
84
- stack: 'Stack 0',
85
- },
86
- ],
87
- }
88
-
89
- console.log(data)
90
- const config = {
91
- responsive:true,
92
- maintainAspectRatio: false,
93
- plugins: [ChartDataLabels],
94
- type: 'bar',
95
- data: data,
96
- options: {
97
- layout: {
98
- padding: 100,
99
- },
100
- barThickness: calcBarThickness,
101
- plugins: {
102
- legend: {
103
- display: false,
104
- },
105
- tooltip: {
106
- mode: 'index',
107
- intersect: true,
108
- position: 'average',
109
- },
110
- datalabels: {
111
- anchor: 'end',
112
- align: 'end',
113
- borderRadius: 4,
114
- padding: { top: 8, bottom: 8, right: 8, left: 8 },
115
- font: { size: 12, weight: 'normal', lineHeight: 1.6, family: fontFamily },
116
- textAlign: 'center',
117
- },
118
- },
119
- scales: {
120
- y: { display: false },
121
- x: {
122
- position: 'bottom',
123
- display: true,
124
- border: { display: false },
125
- grid: { drawBorder: false, display: false },
126
- ticks: {
127
- padding: 8,
128
- font: { weight: 'bold', family: fontFamily, size: 14 },
129
- color: 'rgb(228, 228, 231)'
130
- },
131
- },
132
- },
31
+ const config = {
32
+ responsive: true,
33
+ maintainAspectRatio: false,
34
+ plugins: [ChartDataLabels],
35
+ type: 'bar',
36
+ data,
37
+ options: {
38
+ layout: {
39
+ padding: 100,
40
+ },
41
+ plugins: {
42
+ legend: {
43
+ display: false,
44
+ },
45
+ tooltip: {
46
+ mode: 'index',
47
+ intersect: true,
48
+ position: 'average',
49
+ },
50
+ datalabels: {
51
+ anchor: 'end',
52
+ align: 'end',
53
+ borderRadius: 4,
54
+ padding: {
55
+ top: 8, bottom: 8, right: 8, left: 8,
56
+ },
57
+ font: {
58
+ size: 12, weight: 'normal', lineHeight: 1.6, family: fontFamily,
59
+ },
60
+ textAlign: 'center',
61
+ },
62
+ },
63
+ scales: {
64
+ y: { display: false },
65
+ x: {
66
+ position: 'bottom',
67
+ display: true,
68
+ border: { display: false },
69
+ grid: { drawBorder: false, display: false },
70
+ ticks: {
71
+ padding: 8,
72
+ font: { weight: 'bold', family: fontFamily, size: 14 },
73
+ color: 'rgb(228, 228, 231)',
133
74
  },
134
- }
75
+ },
76
+ },
77
+ },
78
+ };
135
79
 
136
- const visitorsData = []
80
+ const visitorsData = [];
137
81
 
138
- new Chart(
139
- this.element,
140
- config
141
- );
142
- }
82
+ new Chart(
83
+ this.element,
84
+ config,
85
+ );
86
+ }
143
87
  }
@@ -1,3 +1,4 @@
1
- import {application} from "controllers/application"
2
- import {eagerLoadControllersFrom} from "@hotwired/stimulus-loading"
3
- eagerLoadControllersFrom("controllers", application)
1
+ import { application } from 'controllers/application';
2
+ import { eagerLoadControllersFrom } from '@hotwired/stimulus-loading';
3
+
4
+ eagerLoadControllersFrom('controllers', application);
@@ -0,0 +1,10 @@
1
+ import { Controller } from '@hotwired/stimulus';
2
+
3
+ export default class extends Controller {
4
+ handleChange(event) {
5
+ const url = new URL(event.target.form.action);
6
+ const interval = event.target.value;
7
+ url.searchParams.set('interval', interval);
8
+ event.target.closest('turbo-frame').src = url.href;
9
+ }
10
+ }
@@ -0,0 +1,37 @@
1
+ import { Controller } from '@hotwired/stimulus';
2
+
3
+ export default class extends Controller {
4
+ static values = {
5
+ data: Object,
6
+ label: String
7
+ }
8
+ connect() {
9
+ const getCSS = (varname) => {
10
+ return `hsl(${getComputedStyle(document.documentElement).getPropertyValue(varname)})`
11
+ }
12
+ Chart.register(Chart.Colors)
13
+
14
+ new Chart(this.element,
15
+ {
16
+ type: 'line',
17
+ data: {
18
+ labels: Object.keys(this.dataValue),
19
+ datasets: [
20
+ {
21
+ label: this.labelValue,
22
+ data: Object.values(this.dataValue),
23
+ borderColor: getCSS('--p'),
24
+ backgroundColor: getCSS('--af'),
25
+ color: getCSS('--pc')
26
+ }
27
+ ]
28
+ },
29
+ plugins: {
30
+ colors: {
31
+ forceOverride: true
32
+ }
33
+ }
34
+ },
35
+ )
36
+ }
37
+ }
@@ -0,0 +1,10 @@
1
+ import {Controller} from "@hotwired/stimulus"
2
+ import SlimSelect from 'slim-select'
3
+
4
+ export default class extends Controller {
5
+ static targets = ["select"]
6
+
7
+ handleChange(event) {
8
+ this.selectTarget.name = event.target.value
9
+ }
10
+ }
@@ -1,13 +1,14 @@
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"]
4
+ static targets = ['label'];
5
5
 
6
6
  connect() {
7
- this.reload = this.reload.bind(this)
8
- this.setLabel = this.setLabel.bind(this)
7
+ this.reload = this.reload.bind(this);
8
+ this.setLabel = this.setLabel.bind(this);
9
9
  this.labelCount = 0;
10
-
10
+ this.reloadInterval = setInterval(this.reload, 1000 * 30);
11
+ this.labelInterval = setInterval(this.setLabel, 1000);
11
12
  }
12
13
 
13
14
  reload() {
@@ -17,11 +18,11 @@ export default class extends Controller {
17
18
 
18
19
  setLabel() {
19
20
  this.labelTarget.title = `Last updated ${this.labelCount} seconds ago`;
20
- this.labelCount += 1
21
+ this.labelCount += 1;
21
22
  }
22
23
 
23
24
  disconnect() {
24
- clearInterval(this.labelInterval)
25
- clearInterval(this.reloadInterval)
25
+ clearInterval(this.labelInterval);
26
+ clearInterval(this.reloadInterval);
26
27
  }
27
28
  }
@@ -0,0 +1,65 @@
1
+ import {Controller} from "@hotwired/stimulus"
2
+ import SlimSelect from 'slim-select'
3
+
4
+ export default class extends Controller {
5
+ static values = {
6
+ query: String,
7
+ url: String,
8
+ selected: Array
9
+ }
10
+
11
+ connect() {
12
+ this.loadedInitialData = false;
13
+ this.search = this.search.bind(this)
14
+ this.select = new SlimSelect({
15
+ select: this.element,
16
+ data: [],
17
+ settings: {
18
+ contentPosition: 'relative',
19
+ contentLocation: this.element.closest('fieldset'),
20
+ searchText: 'Sorry, no results found',
21
+ searchPlaceholder: 'Type to populate results',
22
+ placeholderText: `Search`,
23
+ searchHighlight: true
24
+ },
25
+ events: {
26
+ beforeOpen: async () => {
27
+ if (!this.loadedInitialData) {
28
+ const data = await this.search("");
29
+ this.select.setData(data);
30
+ this.loadedInitialData = true
31
+ }
32
+ },
33
+ search: this.search
34
+ }
35
+ });
36
+
37
+ if(this.selectedValue.length) {
38
+ this.select.setData(this.selectedValue.map(item => ({ "text": item, "value": item })))
39
+ this.select.setSelected(this.selectedValue)
40
+ }
41
+ }
42
+
43
+ async search(query) {
44
+ const searchParams = new URLSearchParams(window.location.search);
45
+ const formData = new FormData(this.element.form);
46
+
47
+ let deleted = [];
48
+ for (const [key, value] of formData) {
49
+ if(!deleted.includes(key)) {
50
+ searchParams.delete(key)
51
+ deleted.push(key)
52
+ }
53
+
54
+ searchParams.append(key, value)
55
+ }
56
+
57
+ searchParams.delete(this.element.name);
58
+ searchParams.set(this.queryValue, query);
59
+
60
+ const response = await fetch(`${this.urlValue}?${searchParams.toString()}`);
61
+ const data = await response.json();
62
+ return data;
63
+ }
64
+
65
+ }
@@ -1,13 +1,14 @@
1
1
  <dialog id="<%= id %>" class="modal">
2
- <form method="dialog" class="modal-box w-11/12 max-w-5xl" data-controller='filter' data-action="submit->filter#applyFilters reset->filter#resetFilters">
2
+ <div class="modal-box w-11/12 max-w-5xl">
3
3
  <fieldset>
4
4
  <h5><%= title %></h5>
5
5
  <%= modal_display %>
6
6
  <button class="btn btn-primary mt-4" type="submit">Apply</button>
7
7
  <button class="btn btn-primary mt-4" type="reset">Reset</button>
8
8
  </fieldset>
9
- </form>
10
- <form method="dialog" class="modal-backdrop">
11
- <button>close</button>
12
- </form>
9
+ </div>
10
+ <label class="modal-backdrop">
11
+ <button onclick="event.preventDefault(); <%=id %>.close();">Close</button>
12
+ </label>
13
13
  </dialog>
14
+
@@ -1,21 +1,23 @@
1
- <fieldset class="flex space-x-4 items-end">
1
+ <fieldset class="flex space-x-4 items-end" data-controller="predicate-select">
2
2
  <div class="flex flex-col w-[20%]">
3
3
  <label class="label"><%= label %></label>
4
- <select class='select select-bordered' data-filter-target='predicate' name="<%= column %>">
5
- <% predicates.each do |predicate| %>
6
- <option value="<%= column %>_<%= predicate %>" <%= 'selected' if selected_predicate == "#{column}_#{predicate}" %>>
4
+ <select class='select select-bordered' data-action='change->predicate-select#handleChange'>
5
+ <% predicate_options.each do |predicate| %>
6
+ <option value="<%= option_value(predicate) %>" <%= 'selected' if selected_predicate == predicate %>>
7
7
  <%= predicate.to_s.humanize %>
8
8
  </option>
9
9
  <% end %>
10
10
  </select>
11
11
  </div>
12
+
12
13
  <select
14
+ data-predicate-select-target="select"
15
+ data-controller="search-select"
13
16
  class='w-[50%] border-base-content border-opacity-20 rounded-lg min-h-[3rem]'
14
- name="<%= column %>"
15
- multiple
16
- data-filter-target="select"
17
- data-filter-column-value="<%= column %>"
18
- data-filter-url-value="<%= url %>"
19
- data-filter-selected="<%= values %>"
20
- ></select>
17
+ name="<%= column_name_with_predicate %>"
18
+ <% if multiple %>multiple<% end %>
19
+ data-search-select-query-value="q[<%= column %>_i_cont]"
20
+ data-search-select-url-value="<%= url %>"
21
+ data-search-select-selected-value="<%= values %>"
22
+ ></select>
21
23
  </fieldset>
@@ -1,17 +1,35 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class AhoyCaptain::Filter::SelectComponent < ViewComponent::Base
4
- def initialize(label:, column:, url:, predicates:)
4
+ def initialize(label:, column:, url:, predicates:, form:, multiple: true)
5
5
  @label = label
6
6
  @column = column
7
7
  @url = url
8
8
  @predicates = predicates
9
+ @form = form
10
+ @multiple = multiple
9
11
  end
10
12
 
13
+ private
14
+
11
15
  def selected_predicate
12
16
  predicate_options.detect { |option| params.dig(:q, option) }
13
17
  end
14
18
 
19
+ def option_value(predicate)
20
+ name = "q[#{predicate}]"
21
+ name += "[]" if multiple
22
+ name
23
+ end
24
+
25
+ def column_name_with_predicate
26
+ if selected_predicate
27
+ option_value(selected_predicate)
28
+ else
29
+ option_value(predicate_options.first)
30
+ end
31
+ end
32
+
15
33
  def values
16
34
  predicate_options.each do |predicate|
17
35
  option = params.dig(:q, predicate)
@@ -23,10 +41,9 @@ class AhoyCaptain::Filter::SelectComponent < ViewComponent::Base
23
41
  []
24
42
  end
25
43
 
26
- private
27
-
28
44
  def predicate_options
29
45
  @predicate_options ||= @predicates.map { |predicate| "#{@column}_#{predicate}" }
30
46
  end
31
- attr_reader :label, :column, :url, :predicates
47
+
48
+ attr_reader :label, :column, :url, :predicates, :form, :multiple
32
49
  end