sage-rails 0.0.7 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f84a14a8dc13d5bca60926490250937bc5b52ae4f77c6962e328cbb28a5635a1
4
- data.tar.gz: 59d079bb38465f75b165ed44d63b3cfd9d1f8d52a56fdb5c78feba7cdf987237
3
+ metadata.gz: 0d4771d99542d22d3e3665bd1d633decc10a4b9246fc867d48bb9b6c63598060
4
+ data.tar.gz: bf330ca38aff34abb0a3df19d09d937e2235d659a98f30143082569c1aea9aa3
5
5
  SHA512:
6
- metadata.gz: 71fe7deb5b559a26703f21a3f81f998a4e32a521c91c37e2f69b01957cd39266e098ad679330cfec776408f95b2001dfa79c67f6df5fd9221a55d8e4e826d163
7
- data.tar.gz: 4a30c3d554af61200f3424c30c00ab51263229470c8977799289f30fee9dac43605b867f7e7719af9b82c7c995288a13f493f289ccd393ad09198c01b70fcaa1
6
+ metadata.gz: db485b33ee21c595f40eb29e9d0e9aa62514ddd67db154ee0022757934ceedd7382183bef532f3566a4c13f6ec5d0a3bf9a2f44d6fd47835c665888752fd9d80
7
+ data.tar.gz: 51bcfe5e82dc8350c5c6294b75b1859e9e0bc6f24ed175d06c950a849c57b416fd86ad7956940c2a498c7cc10d1883d758382345e64e2161762cee62a7cc2d01
data/README.md CHANGED
@@ -10,6 +10,8 @@ Sage is a Rails engine built on top of the excellent [Blazer](https://github.com
10
10
 
11
11
  ## Requirements
12
12
 
13
+ Rails 7.1+ is recommended, compatability with older versions not guaranteed.
14
+
13
15
  Sage relies on `turbo-rails` and `stimulus-rails` to render the dashboard. These gems should already be included in most modern Rails applications. If not, add them to your Gemfile:
14
16
 
15
17
  ```ruby
@@ -45,7 +47,6 @@ This generator will:
45
47
  - Mount Sage at `/sage` in your routes
46
48
  - Create an initializer at `config/initializers/sage.rb`
47
49
  - Set up database migrations for message storage
48
- - Configure JavaScript and CSS dependencies
49
50
 
50
51
  After installation, run the migrations:
51
52
  ```bash
@@ -0,0 +1,26 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ export default class extends Controller {
4
+ static values = { text: String }
5
+
6
+ connect() {
7
+ console.log("Clipboard controller connected");
8
+ }
9
+
10
+ copy(event) {
11
+ event.preventDefault()
12
+
13
+ navigator.clipboard.writeText(this.textValue).then(() => {
14
+ const originalText = event.target.textContent
15
+ event.target.textContent = "Copied!"
16
+ event.target.classList.add("btn-success")
17
+
18
+ setTimeout(() => {
19
+ event.target.textContent = originalText
20
+ event.target.classList.remove("btn-success")
21
+ }, 2000)
22
+ }).catch(err => {
23
+ console.error('Failed to copy text: ', err)
24
+ })
25
+ }
26
+ }
@@ -0,0 +1,132 @@
1
+ import { Controller } from "@hotwired/stimulus";
2
+
3
+ // Connects to data-controller="dashboard"
4
+ export default class extends Controller {
5
+ static targets = ["queryList", "queryTemplate"];
6
+ static values = { queries: Array };
7
+
8
+ connect() {
9
+ console.log("Dashboard controller connected");
10
+ console.log("Queries value:", this.queriesValue);
11
+
12
+ // Parse the queries value if it's a string
13
+ const queriesData = typeof this.queriesValue === 'string' ?
14
+ JSON.parse(this.queriesValue) : this.queriesValue;
15
+ this.selectedQueries = Array.isArray(queriesData) ? [...queriesData] : [];
16
+
17
+ console.log("Selected queries:", this.selectedQueries);
18
+
19
+ this.render();
20
+ this.setupSortable();
21
+ }
22
+
23
+ addQuery(event) {
24
+ const { value, text } = event.detail;
25
+
26
+ // Check for duplicates and remove if found
27
+ const existingIndex = this.selectedQueries.findIndex(q => q.id == value);
28
+ if (existingIndex !== -1) {
29
+ this.selectedQueries.splice(existingIndex, 1);
30
+ }
31
+
32
+ // Add the new query
33
+ this.selectedQueries.push({ id: value, name: text });
34
+ this.render();
35
+ }
36
+
37
+ removeQuery(event) {
38
+ const index = parseInt(event.params.index);
39
+ this.selectedQueries.splice(index, 1);
40
+ this.render();
41
+ }
42
+
43
+ render() {
44
+ // Check if the target exists
45
+ if (!this.hasQueryListTarget) {
46
+ console.error("queryList target not found");
47
+ return;
48
+ }
49
+
50
+ if (this.selectedQueries.length === 0) {
51
+ this.queryListTarget.style.display = 'none';
52
+ return;
53
+ }
54
+
55
+ this.queryListTarget.style.display = 'block';
56
+ let queriesContainer = this.queryListTarget.querySelector('#queries');
57
+
58
+ // If the container doesn't exist, create it
59
+ if (!queriesContainer) {
60
+ queriesContainer = document.createElement('div');
61
+ queriesContainer.id = 'queries';
62
+ queriesContainer.style.display = 'flex';
63
+ queriesContainer.style.flexDirection = 'column';
64
+ queriesContainer.style.gap = '8px';
65
+ this.queryListTarget.appendChild(queriesContainer);
66
+ }
67
+
68
+ queriesContainer.innerHTML = '';
69
+
70
+ this.selectedQueries.forEach((query, index) => {
71
+ // Create a plain div without any Beer CSS classes
72
+ const item = document.createElement('div');
73
+ // Remove ALL classes and use inline styles only
74
+ item.style.cssText = `
75
+ display: flex !important;
76
+ align-items: center !important;
77
+ width: 100% !important;
78
+ padding: 12px !important;
79
+ margin-bottom: 8px !important;
80
+ background-color: #f5f5f5 !important;
81
+ border-radius: 8px !important;
82
+ opacity: 1 !important;
83
+ filter: none !important;
84
+ `;
85
+
86
+ // Use a Material Icons checkbox instead of HTML input
87
+ const checkIcon = document.createElement('i');
88
+ checkIcon.className = 'material-icons';
89
+ checkIcon.textContent = 'check_box';
90
+ checkIcon.style.cssText = 'color: #6750A4 !important; margin-right: 12px !important; font-size: 24px !important;';
91
+
92
+ const text = document.createElement('strong'); // Use strong tag
93
+ text.textContent = query.name;
94
+ text.style.cssText = 'flex: 1 !important; color: #000000 !important; opacity: 1 !important; filter: none !important;';
95
+
96
+ const closeBtn = document.createElement('i');
97
+ closeBtn.className = 'material-icons';
98
+ closeBtn.textContent = 'close';
99
+ closeBtn.style.cssText = 'cursor: pointer !important; color: #000000 !important; opacity: 1 !important; font-size: 20px !important;';
100
+ closeBtn.setAttribute('data-action', 'click->sage--dashboard#removeQuery');
101
+ closeBtn.setAttribute('data-sage--dashboard-index-param', index);
102
+
103
+ const hiddenInput = document.createElement('input');
104
+ hiddenInput.type = 'hidden';
105
+ hiddenInput.name = 'query_ids[]';
106
+ hiddenInput.value = query.id;
107
+
108
+ item.appendChild(checkIcon);
109
+ item.appendChild(text);
110
+ item.appendChild(closeBtn);
111
+ item.appendChild(hiddenInput);
112
+
113
+ queriesContainer.appendChild(item);
114
+ });
115
+ }
116
+
117
+ setupSortable() {
118
+ if (typeof Sortable !== 'undefined') {
119
+ const queriesContainer = this.queryListTarget.querySelector('#queries');
120
+ if (queriesContainer) {
121
+ Sortable.create(queriesContainer, {
122
+ onEnd: (e) => {
123
+ // Move the item in our array to match the new position
124
+ const movedItem = this.selectedQueries.splice(e.oldIndex, 1)[0];
125
+ this.selectedQueries.splice(e.newIndex, 0, movedItem);
126
+ this.render();
127
+ }
128
+ });
129
+ }
130
+ }
131
+ }
132
+ }
@@ -0,0 +1,146 @@
1
+ import { Controller } from "@hotwired/stimulus";
2
+
3
+ console.log("Reverse infinite scroll controller file loaded!");
4
+
5
+ export default class extends Controller {
6
+ static targets = ["entries", "pagination"];
7
+ static values = {
8
+ url: String,
9
+ page: Number,
10
+ loading: Boolean,
11
+ };
12
+
13
+ initialize() {
14
+ console.log("Reverse infinite scroll controller initialized");
15
+ this.intersectionObserver = new IntersectionObserver(
16
+ (entries) => {
17
+ entries.forEach((entry) => {
18
+ if (entry.isIntersecting) {
19
+ console.log("Pagination target is visible - loading older messages");
20
+ this.loadOlder();
21
+ }
22
+ });
23
+ },
24
+ {
25
+ rootMargin: "50px 0px 50px 0px",
26
+ threshold: 0.1,
27
+ }
28
+ );
29
+ }
30
+
31
+ connect() {
32
+ console.log("Reverse infinite scroll controller connected");
33
+ console.log("Initial page value:", this.pageValue);
34
+
35
+ if (this.hasPaginationTarget) {
36
+ console.log("Found pagination target, observing...");
37
+ this.intersectionObserver.observe(this.paginationTarget);
38
+ } else {
39
+ console.log("No pagination target found");
40
+ }
41
+ }
42
+
43
+ paginationTargetConnected(target) {
44
+ console.log("Pagination target connected dynamically, observing...");
45
+ // Add a delay to allow scroll-to-bottom to happen first
46
+ setTimeout(() => {
47
+ console.log("Starting to observe pagination target after delay");
48
+ this.intersectionObserver.observe(target);
49
+ }, 1000);
50
+ }
51
+
52
+ paginationTargetDisconnected(target) {
53
+ console.log("Pagination target disconnected, stop observing...");
54
+ this.intersectionObserver.unobserve(target);
55
+ }
56
+
57
+ disconnect() {
58
+ console.log("Reverse infinite scroll controller disconnected");
59
+ this.intersectionObserver.disconnect();
60
+ }
61
+
62
+ buildUrl(nextPage) {
63
+ const url = new URL(this.urlValue, window.location.origin);
64
+ url.searchParams.set("page", nextPage);
65
+ return url.toString();
66
+ }
67
+
68
+ async loadOlder() {
69
+ if (this.loadingValue) {
70
+ console.log("Already loading, skipping...");
71
+ return;
72
+ }
73
+
74
+ console.log("Loading older messages...");
75
+ console.log("Current pageValue:", this.pageValue);
76
+ this.loadingValue = true;
77
+ const nextPage = this.pageValue + 1;
78
+ console.log("Requesting page:", nextPage);
79
+
80
+ // Store current scroll position
81
+ const oldScrollHeight = this.element.scrollHeight;
82
+ const oldScrollTop = this.element.scrollTop;
83
+
84
+ try {
85
+ const url = this.buildUrl(nextPage);
86
+ console.log("Fetching URL:", url);
87
+
88
+ const response = await fetch(url, {
89
+ headers: {
90
+ Accept: "text/vnd.turbo-stream.html",
91
+ },
92
+ });
93
+
94
+ if (!response.ok) {
95
+ throw new Error(`HTTP error! status: ${response.status}`);
96
+ }
97
+
98
+ const html = await response.text();
99
+ console.log("Received response for page", nextPage);
100
+
101
+ // Parse the turbo-stream content
102
+ const parser = new DOMParser();
103
+ const doc = parser.parseFromString(html, "text/html");
104
+ const template = doc.querySelector("template");
105
+
106
+ if (template) {
107
+ // Remove the current pagination target before appending new content
108
+ if (this.hasPaginationTarget) {
109
+ this.paginationTarget.remove();
110
+ }
111
+
112
+ // Manually prepend the template content (for reverse infinite scroll)
113
+ this.entriesTarget.insertAdjacentHTML("afterbegin", template.innerHTML);
114
+ this.pageValue = nextPage;
115
+ console.log(`Updated to page ${nextPage}`);
116
+
117
+ // Maintain scroll position (prevent jumping to top)
118
+ setTimeout(() => {
119
+ const newScrollHeight = this.element.scrollHeight;
120
+ const heightDifference = newScrollHeight - oldScrollHeight;
121
+ this.element.scrollTop = oldScrollTop + heightDifference;
122
+
123
+ console.log("Scroll position updated:", {
124
+ oldScrollHeight,
125
+ newScrollHeight,
126
+ heightDifference,
127
+ newScrollTop: this.element.scrollTop
128
+ });
129
+
130
+ // Set up observer for new pagination target if it exists
131
+ if (this.hasPaginationTarget) {
132
+ console.log("Found new pagination target, observing...");
133
+ this.intersectionObserver.observe(this.paginationTarget);
134
+ } else {
135
+ console.log("No more pages to load");
136
+ }
137
+ }, 50);
138
+ }
139
+ } catch (error) {
140
+ console.error("Error loading older messages:", error);
141
+ console.log("Staying on page", this.pageValue, "due to error");
142
+ } finally {
143
+ this.loadingValue = false;
144
+ }
145
+ }
146
+ }
@@ -0,0 +1,47 @@
1
+ import { Controller } from "@hotwired/stimulus";
2
+ import debounce from "debounce";
3
+
4
+ // Connects to data-controller="search"
5
+ export default class extends Controller {
6
+ initialize() {
7
+ this.submit = debounce(this.submit.bind(this), 300);
8
+ this.searchInput = null;
9
+ }
10
+
11
+ connect() {
12
+ console.log("Search controller initialized");
13
+ // Find the search input
14
+ this.searchInput = this.element.querySelector('input[type="search"]');
15
+
16
+ if (this.searchInput) {
17
+ // Focus the input
18
+ if (this.searchInput.value.length > 0) {
19
+ this.searchInput.focus();
20
+ }
21
+
22
+ // If we have a stored cursor position, restore it
23
+ if (this.cursorPosition !== undefined) {
24
+ this.searchInput.setSelectionRange(
25
+ this.cursorPosition,
26
+ this.cursorPosition
27
+ );
28
+ // Clear the stored position after using it
29
+ this.cursorPosition = undefined;
30
+ } else {
31
+ // If no stored position, place cursor at the end
32
+ const length = this.searchInput.value.length;
33
+ this.searchInput.setSelectionRange(length, length);
34
+ }
35
+ }
36
+ }
37
+
38
+ submit(event) {
39
+ // Store the cursor position before submitting
40
+ if (this.searchInput) {
41
+ this.cursorPosition = this.searchInput.selectionStart;
42
+ }
43
+
44
+ this.element.requestSubmit();
45
+ }
46
+ }
47
+
@@ -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
+
@@ -20,7 +20,6 @@ module Sage
20
20
  end
21
21
 
22
22
  def add_routes
23
- # Remove existing Blazer route if present
24
23
  routes_file = "config/routes.rb"
25
24
  if File.exist?(routes_file)
26
25
  routes_content = File.read(routes_file)
@@ -29,15 +28,19 @@ module Sage
29
28
  blazer_route_pattern = /^\s*mount\s+Blazer::Engine\s*,\s*at:\s*['"]blazer['"]\s*$/
30
29
 
31
30
  if routes_content.match?(blazer_route_pattern)
32
- # Remove the Blazer route
33
- gsub_file routes_file, blazer_route_pattern, ""
34
- say "Removed existing Blazer route", :yellow
31
+ # Replace the Blazer route with Sage route
32
+ gsub_file routes_file, blazer_route_pattern, ' mount Sage::Engine => "/sage"'
33
+ say "Replaced Blazer route with Sage route at /sage", :green
34
+ else
35
+ # No existing Blazer route, add Sage route
36
+ route 'mount Sage::Engine => "/sage"'
37
+ say "Mounted Sage at /sage", :green
35
38
  end
39
+ else
40
+ # No routes file, add Sage route
41
+ route 'mount Sage::Engine => "/sage"'
42
+ say "Mounted Sage at /sage", :green
36
43
  end
37
-
38
- # Mount Sage (which includes Blazer functionality)
39
- route 'mount Sage::Engine => "/sage"'
40
- say "Mounted Sage at /sage", :green
41
44
  end
42
45
 
43
46
  def create_initializer
@@ -72,31 +75,6 @@ module Sage
72
75
  say "Created migration for sage_messages table", :green
73
76
  end
74
77
 
75
- def add_javascript_integration
76
- say "Configuring JavaScript integration...", :green
77
-
78
- # Update controllers/index.js to register Sage controllers
79
- controllers_index_path = "app/javascript/controllers/index.js"
80
- if File.exist?(controllers_index_path)
81
- controllers_content = File.read(controllers_index_path)
82
- unless controllers_content.include?("sage")
83
- append_to_file controllers_index_path do
84
- <<~JS
85
-
86
- // Import and register Sage controllers
87
- import { registerControllers } from "sage"
88
- registerControllers(application)
89
- JS
90
- end
91
- say "Updated controllers/index.js to register Sage controllers", :green
92
- else
93
- say "Sage controllers already registered in controllers/index.js", :yellow
94
- end
95
- else
96
- say "Could not find app/javascript/controllers/index.js - you'll need to manually import Sage controllers", :yellow
97
- end
98
- end
99
-
100
78
  def add_stylesheets
101
79
  # Stylesheets are served directly from the engine via the asset pipeline
102
80
  # No need to copy or require them - they're automatically available
data/lib/sage/engine.rb CHANGED
@@ -141,8 +141,8 @@ module Sage
141
141
  end
142
142
 
143
143
  initializer "sage.assets" do |app|
144
- # Add JavaScript path for Propshaft
145
- if defined?(Propshaft::Railtie)
144
+ # Add JavaScript paths based on asset pipeline
145
+ if app.config.respond_to?(:assets)
146
146
  app.config.assets.paths << Engine.root.join("app/javascript")
147
147
  app.config.assets.paths << Engine.root.join("app/javascript/sage")
148
148
  end
data/lib/sage/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Sage
2
- VERSION = "0.0.7"
2
+ VERSION = "0.0.8"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sage-rails
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.7
4
+ version: 0.0.8
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nathan Jones
@@ -148,7 +148,14 @@ files:
148
148
  - app/assets/images/sage/chevron-down-zinc-500.svg
149
149
  - app/assets/images/sage/chevron-right.svg
150
150
  - app/assets/images/sage/loading.svg
151
+ - app/assets/javascripts/sage.js
151
152
  - app/assets/javascripts/sage/application.js
153
+ - app/assets/javascripts/sage/controllers/clipboard_controller.js
154
+ - app/assets/javascripts/sage/controllers/dashboard_controller.js
155
+ - app/assets/javascripts/sage/controllers/reverse_infinite_scroll_controller.js
156
+ - app/assets/javascripts/sage/controllers/search_controller.js
157
+ - app/assets/javascripts/sage/controllers/select_controller.js
158
+ - app/assets/javascripts/sage/controllers/variables_controller.js
152
159
  - app/assets/stylesheets/sage/application.css
153
160
  - app/controllers/sage/actions_controller.rb
154
161
  - app/controllers/sage/application_controller.rb