sage-rails 0.0.6 → 0.0.8
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.
- checksums.yaml +4 -4
- data/CONTRIBUTING.md +42 -0
- data/README.md +21 -22
- data/app/assets/javascripts/sage/application.js +27 -1
- data/app/assets/javascripts/sage/controllers/clipboard_controller.js +26 -0
- data/app/assets/javascripts/sage/controllers/dashboard_controller.js +132 -0
- data/app/assets/javascripts/sage/controllers/reverse_infinite_scroll_controller.js +146 -0
- data/app/assets/javascripts/sage/controllers/search_controller.js +47 -0
- data/app/assets/javascripts/sage/controllers/select_controller.js +215 -0
- data/app/assets/javascripts/sage/controllers/variables_controller.js +122 -0
- data/app/assets/javascripts/sage.js +21 -0
- data/app/controllers/sage/checks_controller.rb +3 -3
- data/app/controllers/sage/dashboards_controller.rb +8 -9
- data/app/controllers/sage/queries_controller.rb +1 -1
- data/app/helpers/sage/application_helper.rb +1 -1
- data/app/helpers/sage/queries_helper.rb +2 -2
- data/app/javascript/sage/controllers/variables_controller.js +122 -0
- data/app/javascript/sage.js +3 -1
- data/app/views/sage/_variables.html.erb +168 -0
- data/app/views/sage/dashboards/show.html.erb +1 -1
- data/app/views/sage/queries/show.html.erb +2 -2
- data/config/initializers/ransack.rb +19 -19
- data/config/routes.rb +1 -1
- data/lib/generators/sage/install/install_generator.rb +11 -33
- data/lib/sage/engine.rb +2 -2
- data/lib/sage/model_scopes_context.rb +19 -19
- data/lib/sage/version.rb +1 -1
- metadata +11 -1
@@ -0,0 +1,215 @@
|
|
1
|
+
import { Controller } from "@hotwired/stimulus";
|
2
|
+
|
3
|
+
// Connects to data-controller="select"
|
4
|
+
export default class extends Controller {
|
5
|
+
static targets = ["input", "dropdown", "option", "hidden"];
|
6
|
+
static values = {
|
7
|
+
options: Array,
|
8
|
+
placeholder: String,
|
9
|
+
selected: String,
|
10
|
+
maxOptions: { type: Number, default: 100 }
|
11
|
+
};
|
12
|
+
|
13
|
+
connect() {
|
14
|
+
this.selectedValue = this.selectedValue || "";
|
15
|
+
|
16
|
+
// Parse options if they're a string
|
17
|
+
let options = this.optionsValue;
|
18
|
+
if (typeof options === 'string') {
|
19
|
+
try {
|
20
|
+
options = JSON.parse(options);
|
21
|
+
} catch (e) {
|
22
|
+
console.error("Failed to parse options:", e);
|
23
|
+
options = [];
|
24
|
+
}
|
25
|
+
}
|
26
|
+
|
27
|
+
this.filteredOptions = Array.isArray(options) ? options.slice(0, this.maxOptionsValue) : [];
|
28
|
+
|
29
|
+
// If there's a pre-selected value from the data attribute, set it
|
30
|
+
if (this.hasSelectedValue && this.selectedValue) {
|
31
|
+
const selectedOption = this.filteredOptions.find(opt => opt.value == this.selectedValue);
|
32
|
+
if (selectedOption) {
|
33
|
+
this.inputTarget.value = selectedOption.text;
|
34
|
+
if (this.hasHiddenTarget) {
|
35
|
+
this.hiddenTarget.value = selectedOption.value;
|
36
|
+
}
|
37
|
+
}
|
38
|
+
}
|
39
|
+
|
40
|
+
this.render();
|
41
|
+
this.setupEventListeners();
|
42
|
+
}
|
43
|
+
|
44
|
+
setupEventListeners() {
|
45
|
+
// Close dropdown when clicking outside
|
46
|
+
document.addEventListener('click', this.handleOutsideClick.bind(this));
|
47
|
+
|
48
|
+
// Handle keyboard navigation
|
49
|
+
this.inputTarget.addEventListener('keydown', this.handleKeydown.bind(this));
|
50
|
+
}
|
51
|
+
|
52
|
+
disconnect() {
|
53
|
+
document.removeEventListener('click', this.handleOutsideClick.bind(this));
|
54
|
+
}
|
55
|
+
|
56
|
+
handleOutsideClick(event) {
|
57
|
+
if (!this.element.contains(event.target)) {
|
58
|
+
this.closeDropdown();
|
59
|
+
}
|
60
|
+
}
|
61
|
+
|
62
|
+
handleKeydown(event) {
|
63
|
+
const dropdown = this.dropdownTarget;
|
64
|
+
const options = this.optionTargets;
|
65
|
+
const activeOption = dropdown.querySelector('.active');
|
66
|
+
|
67
|
+
switch(event.key) {
|
68
|
+
case 'ArrowDown':
|
69
|
+
event.preventDefault();
|
70
|
+
this.navigateOptions(options, activeOption, 1);
|
71
|
+
break;
|
72
|
+
case 'ArrowUp':
|
73
|
+
event.preventDefault();
|
74
|
+
this.navigateOptions(options, activeOption, -1);
|
75
|
+
break;
|
76
|
+
case 'Enter':
|
77
|
+
event.preventDefault();
|
78
|
+
if (activeOption) {
|
79
|
+
this.selectOption(activeOption.dataset.value, activeOption.textContent);
|
80
|
+
}
|
81
|
+
break;
|
82
|
+
case 'Escape':
|
83
|
+
this.closeDropdown();
|
84
|
+
break;
|
85
|
+
}
|
86
|
+
}
|
87
|
+
|
88
|
+
navigateOptions(options, activeOption, direction) {
|
89
|
+
let currentIndex = activeOption ? Array.from(options).indexOf(activeOption) : -1;
|
90
|
+
let nextIndex = currentIndex + direction;
|
91
|
+
|
92
|
+
if (nextIndex < 0) nextIndex = options.length - 1;
|
93
|
+
if (nextIndex >= options.length) nextIndex = 0;
|
94
|
+
|
95
|
+
// Remove active class from all options
|
96
|
+
options.forEach(option => option.classList.remove('active'));
|
97
|
+
|
98
|
+
// Add active class to next option
|
99
|
+
if (options[nextIndex]) {
|
100
|
+
options[nextIndex].classList.add('active');
|
101
|
+
options[nextIndex].scrollIntoView({ block: 'nearest' });
|
102
|
+
}
|
103
|
+
}
|
104
|
+
|
105
|
+
search(event) {
|
106
|
+
const query = event.target.value.toLowerCase().trim();
|
107
|
+
|
108
|
+
// Parse options properly
|
109
|
+
let options = this.optionsValue;
|
110
|
+
if (typeof options === 'string') {
|
111
|
+
try {
|
112
|
+
options = JSON.parse(options);
|
113
|
+
} catch (e) {
|
114
|
+
options = [];
|
115
|
+
}
|
116
|
+
}
|
117
|
+
options = Array.isArray(options) ? options : [];
|
118
|
+
|
119
|
+
if (query === '' || query === ' ') {
|
120
|
+
this.filteredOptions = options.slice(0, this.maxOptionsValue);
|
121
|
+
} else {
|
122
|
+
this.filteredOptions = options
|
123
|
+
.filter(option => option.text.toLowerCase().includes(query))
|
124
|
+
.slice(0, this.maxOptionsValue);
|
125
|
+
}
|
126
|
+
|
127
|
+
this.renderOptions();
|
128
|
+
this.openDropdown();
|
129
|
+
}
|
130
|
+
|
131
|
+
selectOption(value, text) {
|
132
|
+
// Update the input to show selected text
|
133
|
+
this.inputTarget.value = text;
|
134
|
+
this.selectedValue = value;
|
135
|
+
|
136
|
+
// Update hidden field if present
|
137
|
+
if (this.hasHiddenTarget) {
|
138
|
+
this.hiddenTarget.value = value;
|
139
|
+
}
|
140
|
+
|
141
|
+
// Dispatch custom event for external handling
|
142
|
+
const selectEvent = new CustomEvent('select:change', {
|
143
|
+
detail: { value: value, text: text },
|
144
|
+
bubbles: true
|
145
|
+
});
|
146
|
+
this.element.dispatchEvent(selectEvent);
|
147
|
+
|
148
|
+
this.closeDropdown();
|
149
|
+
}
|
150
|
+
|
151
|
+
openDropdown() {
|
152
|
+
this.dropdownTarget.classList.remove('hidden');
|
153
|
+
this.dropdownTarget.classList.add('visible');
|
154
|
+
}
|
155
|
+
|
156
|
+
closeDropdown() {
|
157
|
+
this.dropdownTarget.classList.remove('visible');
|
158
|
+
this.dropdownTarget.classList.add('hidden');
|
159
|
+
}
|
160
|
+
|
161
|
+
focus() {
|
162
|
+
// Reset search to show all options when focusing
|
163
|
+
const currentValue = this.inputTarget.value.trim();
|
164
|
+
if (currentValue === '' || currentValue === ' ') {
|
165
|
+
// Parse options properly
|
166
|
+
let options = this.optionsValue;
|
167
|
+
if (typeof options === 'string') {
|
168
|
+
try {
|
169
|
+
options = JSON.parse(options);
|
170
|
+
} catch (e) {
|
171
|
+
options = [];
|
172
|
+
}
|
173
|
+
}
|
174
|
+
this.filteredOptions = Array.isArray(options) ? options.slice(0, this.maxOptionsValue) : [];
|
175
|
+
this.renderOptions();
|
176
|
+
}
|
177
|
+
this.openDropdown();
|
178
|
+
}
|
179
|
+
|
180
|
+
blur() {
|
181
|
+
// Delay hiding to allow option clicks
|
182
|
+
setTimeout(() => {
|
183
|
+
this.closeDropdown();
|
184
|
+
}, 150);
|
185
|
+
}
|
186
|
+
|
187
|
+
render() {
|
188
|
+
// Don't set placeholder for Beer CSS floating labels
|
189
|
+
this.renderOptions();
|
190
|
+
}
|
191
|
+
|
192
|
+
renderOptions() {
|
193
|
+
this.dropdownTarget.innerHTML = '';
|
194
|
+
|
195
|
+
if (this.filteredOptions.length === 0) {
|
196
|
+
const noResults = document.createElement('div');
|
197
|
+
noResults.className = 'select-option no-results';
|
198
|
+
noResults.textContent = 'No results found';
|
199
|
+
this.dropdownTarget.appendChild(noResults);
|
200
|
+
return;
|
201
|
+
}
|
202
|
+
|
203
|
+
this.filteredOptions.forEach(option => {
|
204
|
+
const optionElement = document.createElement('div');
|
205
|
+
optionElement.className = 'select-option';
|
206
|
+
optionElement.dataset.selectTarget = 'option';
|
207
|
+
optionElement.dataset.value = option.value;
|
208
|
+
optionElement.textContent = option.text;
|
209
|
+
optionElement.addEventListener('click', () => {
|
210
|
+
this.selectOption(option.value, option.text);
|
211
|
+
});
|
212
|
+
this.dropdownTarget.appendChild(optionElement);
|
213
|
+
});
|
214
|
+
}
|
215
|
+
}
|
@@ -0,0 +1,122 @@
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
2
|
+
|
3
|
+
export default class extends Controller {
|
4
|
+
static targets = ["form", "variable"]
|
5
|
+
|
6
|
+
connect() {
|
7
|
+
// Override the global submitIfCompleted function from Blazer
|
8
|
+
window.submitIfCompleted = this.submitIfCompleted.bind(this)
|
9
|
+
|
10
|
+
// Set up event listeners for variable changes
|
11
|
+
this.setupVariableListeners()
|
12
|
+
}
|
13
|
+
|
14
|
+
setupVariableListeners() {
|
15
|
+
// Listen for changes on all variable inputs
|
16
|
+
this.variableTargets.forEach(input => {
|
17
|
+
input.addEventListener("change", (event) => {
|
18
|
+
this.handleVariableChange(event)
|
19
|
+
})
|
20
|
+
|
21
|
+
// Also listen for daterangepicker events if present
|
22
|
+
if (input.dataset.daterangepicker) {
|
23
|
+
const picker = $(input).data('daterangepicker')
|
24
|
+
if (picker) {
|
25
|
+
$(input).on('apply.daterangepicker', (ev, picker) => {
|
26
|
+
this.handleDateRangeChange(input, picker)
|
27
|
+
})
|
28
|
+
}
|
29
|
+
}
|
30
|
+
})
|
31
|
+
}
|
32
|
+
|
33
|
+
handleVariableChange(event) {
|
34
|
+
const input = event.target
|
35
|
+
console.log(`Variable ${input.name} changed to:`, input.value)
|
36
|
+
|
37
|
+
// Check if all required variables are filled and submit if so
|
38
|
+
setTimeout(() => {
|
39
|
+
this.submitIfCompleted(this.formTarget)
|
40
|
+
}, 100) // Small delay to ensure all values are updated
|
41
|
+
}
|
42
|
+
|
43
|
+
handleDateRangeChange(input, picker) {
|
44
|
+
console.log(`Date range updated for ${input.name}:`, input.value)
|
45
|
+
|
46
|
+
// Force update any related hidden fields
|
47
|
+
this.updateRelatedDateFields(input, picker)
|
48
|
+
|
49
|
+
// Submit the form after date range is updated
|
50
|
+
setTimeout(() => {
|
51
|
+
this.submitIfCompleted(this.formTarget)
|
52
|
+
}, 200) // Longer delay for date picker updates
|
53
|
+
}
|
54
|
+
|
55
|
+
updateRelatedDateFields(input, picker) {
|
56
|
+
// If this is a start_time/end_time combo, make sure both are updated
|
57
|
+
if (input.name === "start_time" || input.name === "end_time") {
|
58
|
+
const startTimeInput = this.formTarget.querySelector('input[name="start_time"]')
|
59
|
+
const endTimeInput = this.formTarget.querySelector('input[name="end_time"]')
|
60
|
+
|
61
|
+
if (startTimeInput && endTimeInput && picker) {
|
62
|
+
// Update both fields based on the picker values
|
63
|
+
if (picker.startDate) {
|
64
|
+
startTimeInput.value = picker.startDate.utc().format()
|
65
|
+
}
|
66
|
+
if (picker.endDate) {
|
67
|
+
endTimeInput.value = picker.endDate.endOf("day").utc().format()
|
68
|
+
}
|
69
|
+
}
|
70
|
+
}
|
71
|
+
}
|
72
|
+
|
73
|
+
submitIfCompleted(form) {
|
74
|
+
if (!form) return
|
75
|
+
|
76
|
+
let completed = true
|
77
|
+
const requiredInputs = form.querySelectorAll('input[name], select[name]')
|
78
|
+
|
79
|
+
// Check each required input
|
80
|
+
requiredInputs.forEach(input => {
|
81
|
+
const value = input.value
|
82
|
+
|
83
|
+
// More robust empty check
|
84
|
+
if (this.isEmpty(value)) {
|
85
|
+
completed = false
|
86
|
+
console.log(`Variable ${input.name} is empty:`, value)
|
87
|
+
} else {
|
88
|
+
console.log(`Variable ${input.name} has value:`, value)
|
89
|
+
}
|
90
|
+
})
|
91
|
+
|
92
|
+
console.log(`Form completion check: ${completed ? 'Complete' : 'Incomplete'}`)
|
93
|
+
|
94
|
+
if (completed) {
|
95
|
+
console.log('Submitting form with all variables filled')
|
96
|
+
form.submit()
|
97
|
+
}
|
98
|
+
}
|
99
|
+
|
100
|
+
isEmpty(value) {
|
101
|
+
// More comprehensive empty check
|
102
|
+
return value === null ||
|
103
|
+
value === undefined ||
|
104
|
+
value === "" ||
|
105
|
+
(typeof value === "string" && value.trim() === "")
|
106
|
+
}
|
107
|
+
|
108
|
+
// Manual trigger for testing
|
109
|
+
triggerSubmit() {
|
110
|
+
this.submitIfCompleted(this.formTarget)
|
111
|
+
}
|
112
|
+
|
113
|
+
// Debug method to check current variable states
|
114
|
+
debugVariables() {
|
115
|
+
console.log("=== Current Variable States ===")
|
116
|
+
const inputs = this.formTarget.querySelectorAll('input[name], select[name]')
|
117
|
+
inputs.forEach(input => {
|
118
|
+
console.log(`${input.name}: "${input.value}" (${typeof input.value})`)
|
119
|
+
})
|
120
|
+
console.log("===============================")
|
121
|
+
}
|
122
|
+
}
|
@@ -0,0 +1,21 @@
|
|
1
|
+
// Import controllers
|
2
|
+
import SearchController from "sage/controllers/search_controller"
|
3
|
+
import ClipboardController from "sage/controllers/clipboard_controller"
|
4
|
+
import SelectController from "sage/controllers/select_controller"
|
5
|
+
import DashboardController from "sage/controllers/dashboard_controller"
|
6
|
+
import ReverseInfiniteScrollController from "sage/controllers/reverse_infinite_scroll_controller"
|
7
|
+
import VariablesController from "sage/controllers/variables_controller"
|
8
|
+
|
9
|
+
// Export all Sage controllers for manual registration
|
10
|
+
export { SearchController, ClipboardController, SelectController, DashboardController, ReverseInfiniteScrollController, VariablesController }
|
11
|
+
|
12
|
+
// Register all Sage controllers with the provided Stimulus application
|
13
|
+
export function registerControllers(application) {
|
14
|
+
application.register("sage--search", SearchController)
|
15
|
+
application.register("sage--clipboard", ClipboardController)
|
16
|
+
application.register("sage--select", SelectController)
|
17
|
+
application.register("sage--dashboard", DashboardController)
|
18
|
+
application.register("sage--reverse-infinite-scroll", ReverseInfiniteScrollController)
|
19
|
+
application.register("sage--variables", VariablesController)
|
20
|
+
}
|
21
|
+
|
@@ -5,13 +5,13 @@ module Sage
|
|
5
5
|
def index
|
6
6
|
@q = Blazer::Check.ransack(params[:q])
|
7
7
|
@checks = @q.result.joins(:query).includes(:query)
|
8
|
-
|
8
|
+
|
9
9
|
# Apply basic ordering first
|
10
10
|
@checks = @checks.order("blazer_queries.name, blazer_checks.id")
|
11
|
-
|
11
|
+
|
12
12
|
# Apply pagination with Pagy
|
13
13
|
@pagy, @checks = pagy(@checks)
|
14
|
-
|
14
|
+
|
15
15
|
# Apply state-based sorting on the paginated results
|
16
16
|
state_order = [ nil, "disabled", "error", "timed out", "failing", "passing" ]
|
17
17
|
@checks = @checks.sort_by { |q| state_order.index(q.state) || 99 }
|
@@ -1,21 +1,21 @@
|
|
1
1
|
module Sage
|
2
2
|
class DashboardsController < BaseController
|
3
|
-
before_action :set_dashboard, only: [:show, :edit, :update, :destroy, :refresh]
|
3
|
+
before_action :set_dashboard, only: [ :show, :edit, :update, :destroy, :refresh ]
|
4
4
|
|
5
5
|
def index
|
6
6
|
@q = Blazer::Dashboard.ransack(params[:q])
|
7
7
|
@dashboards = @q.result
|
8
|
-
|
8
|
+
|
9
9
|
# Only include creator if Blazer.user_class is configured
|
10
10
|
@dashboards = @dashboards.includes(:creator) if Blazer.user_class
|
11
|
-
|
11
|
+
|
12
12
|
@dashboards = @dashboards.order(:name)
|
13
|
-
|
13
|
+
|
14
14
|
# Apply additional filters if needed
|
15
15
|
if blazer_user && params[:filter] == "mine"
|
16
16
|
@dashboards = @dashboards.where(creator_id: blazer_user.id).reorder(updated_at: :desc)
|
17
17
|
end
|
18
|
-
|
18
|
+
|
19
19
|
# Apply pagination with Pagy
|
20
20
|
@pagy, @dashboards = pagy(@dashboards)
|
21
21
|
end
|
@@ -40,7 +40,7 @@ module Sage
|
|
40
40
|
def show
|
41
41
|
@queries = @dashboard.dashboard_queries.order(:position).preload(:query).map(&:query)
|
42
42
|
@query_errors = {}
|
43
|
-
|
43
|
+
|
44
44
|
@queries.each do |query|
|
45
45
|
# Check if the query has a valid data source
|
46
46
|
if query.data_source.blank? || !Blazer.data_sources.key?(query.data_source)
|
@@ -53,12 +53,12 @@ module Sage
|
|
53
53
|
|
54
54
|
@smart_vars = {}
|
55
55
|
@sql_errors = []
|
56
|
-
@data_sources = @queries.map { |q|
|
56
|
+
@data_sources = @queries.map { |q|
|
57
57
|
# Use the query's data source if specified, otherwise use the default
|
58
58
|
source = q.data_source.presence || Blazer.data_sources.keys.first
|
59
59
|
Blazer.data_sources[source]
|
60
60
|
}.compact.uniq
|
61
|
-
|
61
|
+
|
62
62
|
@bind_vars.each do |var|
|
63
63
|
@data_sources.each do |data_source|
|
64
64
|
smart_var, error = parse_smart_variables(var, data_source)
|
@@ -127,4 +127,3 @@ module Sage
|
|
127
127
|
end
|
128
128
|
end
|
129
129
|
end
|
130
|
-
|
@@ -150,7 +150,7 @@ module Sage
|
|
150
150
|
|
151
151
|
# fallback for now for users with open tabs
|
152
152
|
# TODO remove fallback in future version
|
153
|
-
@var_params = request.request_parameters["variables"] || request.request_parameters
|
153
|
+
@var_params = params[:variables] || request.request_parameters["variables"] || request.request_parameters
|
154
154
|
@success = process_vars(@statement, @var_params)
|
155
155
|
@only_chart = params[:only_chart]
|
156
156
|
@run_id = blazer_params[:run_id]
|
@@ -0,0 +1,122 @@
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
2
|
+
|
3
|
+
export default class extends Controller {
|
4
|
+
static targets = ["form", "variable"]
|
5
|
+
|
6
|
+
connect() {
|
7
|
+
// Override the global submitIfCompleted function from Blazer
|
8
|
+
window.submitIfCompleted = this.submitIfCompleted.bind(this)
|
9
|
+
|
10
|
+
// Set up event listeners for variable changes
|
11
|
+
this.setupVariableListeners()
|
12
|
+
}
|
13
|
+
|
14
|
+
setupVariableListeners() {
|
15
|
+
// Listen for changes on all variable inputs
|
16
|
+
this.variableTargets.forEach(input => {
|
17
|
+
input.addEventListener("change", (event) => {
|
18
|
+
this.handleVariableChange(event)
|
19
|
+
})
|
20
|
+
|
21
|
+
// Also listen for daterangepicker events if present
|
22
|
+
if (input.dataset.daterangepicker) {
|
23
|
+
const picker = $(input).data('daterangepicker')
|
24
|
+
if (picker) {
|
25
|
+
$(input).on('apply.daterangepicker', (ev, picker) => {
|
26
|
+
this.handleDateRangeChange(input, picker)
|
27
|
+
})
|
28
|
+
}
|
29
|
+
}
|
30
|
+
})
|
31
|
+
}
|
32
|
+
|
33
|
+
handleVariableChange(event) {
|
34
|
+
const input = event.target
|
35
|
+
console.log(`Variable ${input.name} changed to:`, input.value)
|
36
|
+
|
37
|
+
// Check if all required variables are filled and submit if so
|
38
|
+
setTimeout(() => {
|
39
|
+
this.submitIfCompleted(this.formTarget)
|
40
|
+
}, 100) // Small delay to ensure all values are updated
|
41
|
+
}
|
42
|
+
|
43
|
+
handleDateRangeChange(input, picker) {
|
44
|
+
console.log(`Date range updated for ${input.name}:`, input.value)
|
45
|
+
|
46
|
+
// Force update any related hidden fields
|
47
|
+
this.updateRelatedDateFields(input, picker)
|
48
|
+
|
49
|
+
// Submit the form after date range is updated
|
50
|
+
setTimeout(() => {
|
51
|
+
this.submitIfCompleted(this.formTarget)
|
52
|
+
}, 200) // Longer delay for date picker updates
|
53
|
+
}
|
54
|
+
|
55
|
+
updateRelatedDateFields(input, picker) {
|
56
|
+
// If this is a start_time/end_time combo, make sure both are updated
|
57
|
+
if (input.name === "start_time" || input.name === "end_time") {
|
58
|
+
const startTimeInput = this.formTarget.querySelector('input[name="start_time"]')
|
59
|
+
const endTimeInput = this.formTarget.querySelector('input[name="end_time"]')
|
60
|
+
|
61
|
+
if (startTimeInput && endTimeInput && picker) {
|
62
|
+
// Update both fields based on the picker values
|
63
|
+
if (picker.startDate) {
|
64
|
+
startTimeInput.value = picker.startDate.utc().format()
|
65
|
+
}
|
66
|
+
if (picker.endDate) {
|
67
|
+
endTimeInput.value = picker.endDate.endOf("day").utc().format()
|
68
|
+
}
|
69
|
+
}
|
70
|
+
}
|
71
|
+
}
|
72
|
+
|
73
|
+
submitIfCompleted(form) {
|
74
|
+
if (!form) return
|
75
|
+
|
76
|
+
let completed = true
|
77
|
+
const requiredInputs = form.querySelectorAll('input[name], select[name]')
|
78
|
+
|
79
|
+
// Check each required input
|
80
|
+
requiredInputs.forEach(input => {
|
81
|
+
const value = input.value
|
82
|
+
|
83
|
+
// More robust empty check
|
84
|
+
if (this.isEmpty(value)) {
|
85
|
+
completed = false
|
86
|
+
console.log(`Variable ${input.name} is empty:`, value)
|
87
|
+
} else {
|
88
|
+
console.log(`Variable ${input.name} has value:`, value)
|
89
|
+
}
|
90
|
+
})
|
91
|
+
|
92
|
+
console.log(`Form completion check: ${completed ? 'Complete' : 'Incomplete'}`)
|
93
|
+
|
94
|
+
if (completed) {
|
95
|
+
console.log('Submitting form with all variables filled')
|
96
|
+
form.submit()
|
97
|
+
}
|
98
|
+
}
|
99
|
+
|
100
|
+
isEmpty(value) {
|
101
|
+
// More comprehensive empty check
|
102
|
+
return value === null ||
|
103
|
+
value === undefined ||
|
104
|
+
value === "" ||
|
105
|
+
(typeof value === "string" && value.trim() === "")
|
106
|
+
}
|
107
|
+
|
108
|
+
// Manual trigger for testing
|
109
|
+
triggerSubmit() {
|
110
|
+
this.submitIfCompleted(this.formTarget)
|
111
|
+
}
|
112
|
+
|
113
|
+
// Debug method to check current variable states
|
114
|
+
debugVariables() {
|
115
|
+
console.log("=== Current Variable States ===")
|
116
|
+
const inputs = this.formTarget.querySelectorAll('input[name], select[name]')
|
117
|
+
inputs.forEach(input => {
|
118
|
+
console.log(`${input.name}: "${input.value}" (${typeof input.value})`)
|
119
|
+
})
|
120
|
+
console.log("===============================")
|
121
|
+
}
|
122
|
+
}
|
data/app/javascript/sage.js
CHANGED
@@ -4,9 +4,10 @@ import ClipboardController from "sage/controllers/clipboard_controller"
|
|
4
4
|
import SelectController from "sage/controllers/select_controller"
|
5
5
|
import DashboardController from "sage/controllers/dashboard_controller"
|
6
6
|
import ReverseInfiniteScrollController from "sage/controllers/reverse_infinite_scroll_controller"
|
7
|
+
import VariablesController from "sage/controllers/variables_controller"
|
7
8
|
|
8
9
|
// Export all Sage controllers for manual registration
|
9
|
-
export { SearchController, ClipboardController, SelectController, DashboardController, ReverseInfiniteScrollController }
|
10
|
+
export { SearchController, ClipboardController, SelectController, DashboardController, ReverseInfiniteScrollController, VariablesController }
|
10
11
|
|
11
12
|
// Register all Sage controllers with the provided Stimulus application
|
12
13
|
export function registerControllers(application) {
|
@@ -15,5 +16,6 @@ export function registerControllers(application) {
|
|
15
16
|
application.register("sage--select", SelectController)
|
16
17
|
application.register("sage--dashboard", DashboardController)
|
17
18
|
application.register("sage--reverse-infinite-scroll", ReverseInfiniteScrollController)
|
19
|
+
application.register("sage--variables", VariablesController)
|
18
20
|
}
|
19
21
|
|