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
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0d4771d99542d22d3e3665bd1d633decc10a4b9246fc867d48bb9b6c63598060
|
4
|
+
data.tar.gz: bf330ca38aff34abb0a3df19d09d937e2235d659a98f30143082569c1aea9aa3
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: db485b33ee21c595f40eb29e9d0e9aa62514ddd67db154ee0022757934ceedd7382183bef532f3566a4c13f6ec5d0a3bf9a2f44d6fd47835c665888752fd9d80
|
7
|
+
data.tar.gz: 51bcfe5e82dc8350c5c6294b75b1859e9e0bc6f24ed175d06c950a849c57b416fd86ad7956940c2a498c7cc10d1883d758382345e64e2161762cee62a7cc2d01
|
data/CONTRIBUTING.md
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
# Contributing
|
2
|
+
|
3
|
+
First, thanks for wanting to contribute. You’re awesome! :heart:
|
4
|
+
|
5
|
+
## Help
|
6
|
+
|
7
|
+
We’re not able to provide support through GitHub Issues. If you’re looking for help with your code, try posting on [Stack Overflow](https://stackoverflow.com/).
|
8
|
+
|
9
|
+
All features should be documented. If you don’t see a feature in the docs, assume it doesn’t exist.
|
10
|
+
|
11
|
+
## Bugs
|
12
|
+
|
13
|
+
Think you’ve discovered a bug?
|
14
|
+
|
15
|
+
1. Search existing issues to see if it’s been reported.
|
16
|
+
2. Try the `master` branch to make sure it hasn’t been fixed.
|
17
|
+
|
18
|
+
```rb
|
19
|
+
gem "sage-rails", github: "mrjonesbot/sage"
|
20
|
+
```
|
21
|
+
|
22
|
+
If the above steps don’t help, create an issue. Include:
|
23
|
+
|
24
|
+
- Detailed steps to reproduce
|
25
|
+
- Complete backtraces for exceptions
|
26
|
+
|
27
|
+
## New Features
|
28
|
+
|
29
|
+
If you’d like to discuss a new feature, create an issue and start the title with `[Idea]`.
|
30
|
+
|
31
|
+
## Pull Requests
|
32
|
+
|
33
|
+
Fork the project and create a pull request. A few tips:
|
34
|
+
|
35
|
+
- Keep changes to a minimum. If you have multiple features or fixes, submit multiple pull requests.
|
36
|
+
- Follow the existing style. The code should read like it’s written by a single person.
|
37
|
+
|
38
|
+
Feel free to open an issue to get feedback on your idea before spending too much time on it.
|
39
|
+
|
40
|
+
---
|
41
|
+
|
42
|
+
This contributing guide is released under [CCO](https://creativecommons.org/publicdomain/zero/1.0/) (public domain). Use it for your own project without attribution.
|
data/README.md
CHANGED
@@ -2,10 +2,25 @@
|
|
2
2
|
|
3
3
|

|
4
4
|
|
5
|
+
> **Note:** This project is pre-v1 and is subject to frequent changes.
|
6
|
+
|
5
7
|
**Natural language reporting to help your team build accurate reports, faster.**
|
6
8
|
|
7
9
|
Sage is a Rails engine built on top of the excellent [Blazer](https://github.com/ankane/blazer) gem, adding an LLM interface to make data exploration accessible via natural language.
|
8
10
|
|
11
|
+
## Requirements
|
12
|
+
|
13
|
+
Rails 7.1+ is recommended, compatability with older versions not guaranteed.
|
14
|
+
|
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:
|
16
|
+
|
17
|
+
```ruby
|
18
|
+
gem "turbo-rails"
|
19
|
+
gem "stimulus-rails"
|
20
|
+
```
|
21
|
+
|
22
|
+
Refer to the [stimulus-rails installation page](https://github.com/hotwired/stimulus-rails) to get started.
|
23
|
+
|
9
24
|
## Installation
|
10
25
|
|
11
26
|
Add Sage to your application's Gemfile:
|
@@ -32,7 +47,6 @@ This generator will:
|
|
32
47
|
- Mount Sage at `/sage` in your routes
|
33
48
|
- Create an initializer at `config/initializers/sage.rb`
|
34
49
|
- Set up database migrations for message storage
|
35
|
-
- Configure JavaScript and CSS dependencies
|
36
50
|
|
37
51
|
After installation, run the migrations:
|
38
52
|
```bash
|
@@ -88,26 +102,17 @@ For detailed information on Blazer-specific features, refer to the [Blazer docum
|
|
88
102
|
|
89
103
|
## Database Context
|
90
104
|
|
91
|
-
Sage introspects your database schema to provide context for more accurate SQL generation.
|
105
|
+
Sage introspects your database schema to provide context for more accurate SQL generation.
|
92
106
|
|
93
107
|
### Multiple Data Sources
|
94
108
|
|
95
|
-
If you have multiple
|
96
|
-
|
97
|
-
```ruby
|
98
|
-
# config/blazer.yml
|
99
|
-
data_sources:
|
100
|
-
main:
|
101
|
-
url: <%= ENV["DATABASE_URL"] %>
|
102
|
-
analytics:
|
103
|
-
url: <%= ENV["ANALYTICS_DATABASE_URL"] %>
|
104
|
-
```
|
109
|
+
If you have multiple databases, Sage will use the appropriate schema for each:
|
105
110
|
|
106
|
-
When querying
|
111
|
+
When querying different data sources in Blazer, Sage will switch schema context.
|
107
112
|
|
108
113
|
## Model Scope Context
|
109
114
|
|
110
|
-
Sage
|
115
|
+
Sage introspects your Rails model scopes to understand common query patterns, dramatically improving the accuracy of generated SQL queries.
|
111
116
|
|
112
117
|
### Example
|
113
118
|
|
@@ -144,9 +149,8 @@ Sage understands:
|
|
144
149
|
### Benefits
|
145
150
|
|
146
151
|
1. **Business Logic Awareness**: Scopes encode your business rules (what makes a customer "active" or "high-value")
|
147
|
-
2. **
|
148
|
-
3. **
|
149
|
-
4. **Consistency**: Generated queries follow the same patterns as your application code
|
152
|
+
2. **Aggregation Patterns**: Complex scopes with GROUP BY and HAVING clauses guide report generation
|
153
|
+
3. **Consistency**: Generated queries follow the same patterns as your application code
|
150
154
|
|
151
155
|
Scopes now serve dual purposes:
|
152
156
|
1. Reusable query logic in your Rails application
|
@@ -186,11 +190,6 @@ $ rails test
|
|
186
190
|
- Ensure model files are in standard Rails locations (`app/models/`)
|
187
191
|
- Check that Blazer is properly configured and can execute queries
|
188
192
|
|
189
|
-
### Performance
|
190
|
-
- Use lighter models (Claude Haiku, GPT-3.5) for faster response times
|
191
|
-
- Consider caching frequently used queries
|
192
|
-
- Scope context is cached per request to minimize processing
|
193
|
-
|
194
193
|
## Contributing
|
195
194
|
|
196
195
|
Bug reports and pull requests are welcome on GitHub. This project is intended to be a safe, welcoming space for collaboration.
|
@@ -4,7 +4,7 @@ import "@hotwired/turbo-rails"
|
|
4
4
|
import { Application } from "@hotwired/stimulus"
|
5
5
|
|
6
6
|
// Import and register Sage controllers
|
7
|
-
import { SearchController, ClipboardController, SelectController, DashboardController, ReverseInfiniteScrollController } from "sage"
|
7
|
+
import { SearchController, ClipboardController, SelectController, DashboardController, ReverseInfiniteScrollController, VariablesController } from "sage"
|
8
8
|
|
9
9
|
const application = Application.start()
|
10
10
|
application.debug = true
|
@@ -16,3 +16,29 @@ application.register("sage--clipboard", ClipboardController)
|
|
16
16
|
application.register("sage--select", SelectController)
|
17
17
|
application.register("sage--dashboard", DashboardController)
|
18
18
|
application.register("sage--reverse-infinite-scroll", ReverseInfiniteScrollController)
|
19
|
+
application.register("sage--variables", VariablesController)
|
20
|
+
|
21
|
+
// Override Blazer's submitIfCompleted function globally
|
22
|
+
window.submitIfCompleted = function($form) {
|
23
|
+
// Try to find our Sage variables controller first
|
24
|
+
const controller = document.querySelector('[data-controller*="sage--variables"]')
|
25
|
+
if (controller && window.Stimulus) {
|
26
|
+
const controllerInstance = window.Stimulus.getControllerForElementAndIdentifier(controller, 'sage--variables')
|
27
|
+
if (controllerInstance) {
|
28
|
+
controllerInstance.submitIfCompleted($form[0] || $form)
|
29
|
+
return
|
30
|
+
}
|
31
|
+
}
|
32
|
+
|
33
|
+
// Fallback to improved version of original logic
|
34
|
+
var completed = true
|
35
|
+
$form.find("input[name], select").each(function () {
|
36
|
+
const value = $(this).val()
|
37
|
+
if (!value || value.toString().trim() === "") {
|
38
|
+
completed = false
|
39
|
+
}
|
40
|
+
})
|
41
|
+
if (completed) {
|
42
|
+
$form.submit()
|
43
|
+
}
|
44
|
+
}
|
@@ -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
|
+
|