sage-rails 0.0.9 → 0.1.0

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 (27) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/javascripts/sage/controllers/clipboard_controller.js +0 -1
  3. data/app/assets/javascripts/sage/controllers/dashboard_controller.js +1 -7
  4. data/app/assets/javascripts/sage/controllers/query_toggle_controller.js +22 -0
  5. data/app/assets/javascripts/sage/controllers/reverse_infinite_scroll_controller.js +0 -32
  6. data/app/assets/javascripts/sage/controllers/search_controller.js +0 -1
  7. data/app/assets/javascripts/sage/controllers/variables_controller.js +1 -13
  8. data/app/assets/javascripts/sage.js +3 -1
  9. data/app/controllers/sage/queries_controller.rb +2 -70
  10. data/app/javascript/sage/controllers/clipboard_controller.js +0 -1
  11. data/app/javascript/sage/controllers/dashboard_controller.js +0 -3
  12. data/app/javascript/sage/controllers/query_toggle_controller.js +24 -0
  13. data/app/javascript/sage/controllers/reverse_infinite_scroll_controller.js +0 -27
  14. data/app/javascript/sage/controllers/search_controller.js +0 -1
  15. data/app/javascript/sage/controllers/variables_controller.js +0 -11
  16. data/app/javascript/sage.js +3 -1
  17. data/app/views/sage/_variables.html.erb +64 -48
  18. data/app/views/sage/dashboards/index.html.erb +1 -1
  19. data/app/views/sage/queries/_new_form.html.erb +19 -5
  20. data/app/views/sage/queries/_statement_box.html.erb +39 -18
  21. data/app/views/sage/queries/messages/index.html.erb +0 -2
  22. data/app/views/sage/queries/new.html.erb +188 -60
  23. data/app/views/sage/queries/show.html.erb +9 -2
  24. data/config/importmap.rb +2 -0
  25. data/lib/sage/engine.rb +2 -1
  26. data/lib/sage/version.rb +1 -1
  27. metadata +3 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9ea491a1333c2de79c5d62bf5f82afb9826c18215a728865572ab0fd6a060e19
4
- data.tar.gz: 23091f1b922326f6646ce8c0e9e6395b2098789d0eeb8662bea0579b4b897c5d
3
+ metadata.gz: cc7c4334ddc11b1353e8ad25985adb53a4b7cff4ddc03a6f1e6304773f0a3e52
4
+ data.tar.gz: b1c9ee4ea70e918eb1592ae56e0f596edf5e7b08670b8769ca8347d1d2cf7ab0
5
5
  SHA512:
6
- metadata.gz: 7bf466be233a7e4925079033e337aa37719c6be8329145575752f15d8016a9ba9d22a392a5f2b5941049348638b04ed338d3a375e78aa0f214d2c33d1240a02a
7
- data.tar.gz: 1fb3cc033cf1de427baf9d36bf1df9f810b061d363e1df390936c9fef364f30f87d81a4885721aded87d8bc5d91e48ffbb35853a93f822363b2c7ddd7e56bec0
6
+ metadata.gz: 64b12cbfa6ac68335f01a13e0c40d08caf9bbc66ce0a160c8eb277aea82303c43e3fdf4499356b13dcb0688fde80904800b1af752b63b402d966d10b7bf2d90b
7
+ data.tar.gz: 96e9c95b61d505d644859862f5866ebdca3decccb55b54ad03f609541a5f67779474c31f6a4ea688b0889db19494652cd4a82eb2d19d2874370c0d2415739613
@@ -4,7 +4,6 @@ export default class extends Controller {
4
4
  static values = { text: String }
5
5
 
6
6
  connect() {
7
- console.log("Clipboard controller connected");
8
7
  }
9
8
 
10
9
  copy(event) {
@@ -6,16 +6,10 @@ export default class extends Controller {
6
6
  static values = { queries: Array };
7
7
 
8
8
  connect() {
9
- console.log("Dashboard controller connected");
10
- console.log("Queries value:", this.queriesValue);
11
-
12
9
  // Parse the queries value if it's a string
13
10
  const queriesData = typeof this.queriesValue === 'string' ?
14
11
  JSON.parse(this.queriesValue) : this.queriesValue;
15
12
  this.selectedQueries = Array.isArray(queriesData) ? [...queriesData] : [];
16
-
17
- console.log("Selected queries:", this.selectedQueries);
18
-
19
13
  this.render();
20
14
  this.setupSortable();
21
15
  }
@@ -129,4 +123,4 @@ export default class extends Controller {
129
123
  }
130
124
  }
131
125
  }
132
- }
126
+ }
@@ -0,0 +1,22 @@
1
+ import { Controller } from "@hotwired/stimulus";
2
+
3
+ export default class extends Controller {
4
+ static targets = ["content", "icon"]
5
+
6
+ toggle() {
7
+ try {
8
+ const content = this.contentTarget
9
+ const icon = this.iconTarget
10
+
11
+ if (content.style.display === "none") {
12
+ content.style.display = "block"
13
+ icon.textContent = "visibility_off"
14
+ } else {
15
+ content.style.display = "none"
16
+ icon.textContent = "visibility"
17
+ }
18
+ } catch (error) {
19
+ console.error("Error in toggle:", error)
20
+ }
21
+ }
22
+ }
@@ -1,7 +1,5 @@
1
1
  import { Controller } from "@hotwired/stimulus";
2
2
 
3
- console.log("Reverse infinite scroll controller file loaded!");
4
-
5
3
  export default class extends Controller {
6
4
  static targets = ["entries", "pagination"];
7
5
  static values = {
@@ -11,12 +9,10 @@ export default class extends Controller {
11
9
  };
12
10
 
13
11
  initialize() {
14
- console.log("Reverse infinite scroll controller initialized");
15
12
  this.intersectionObserver = new IntersectionObserver(
16
13
  (entries) => {
17
14
  entries.forEach((entry) => {
18
15
  if (entry.isIntersecting) {
19
- console.log("Pagination target is visible - loading older messages");
20
16
  this.loadOlder();
21
17
  }
22
18
  });
@@ -29,33 +25,24 @@ export default class extends Controller {
29
25
  }
30
26
 
31
27
  connect() {
32
- console.log("Reverse infinite scroll controller connected");
33
- console.log("Initial page value:", this.pageValue);
34
-
35
28
  if (this.hasPaginationTarget) {
36
- console.log("Found pagination target, observing...");
37
29
  this.intersectionObserver.observe(this.paginationTarget);
38
30
  } else {
39
- console.log("No pagination target found");
40
31
  }
41
32
  }
42
33
 
43
34
  paginationTargetConnected(target) {
44
- console.log("Pagination target connected dynamically, observing...");
45
35
  // Add a delay to allow scroll-to-bottom to happen first
46
36
  setTimeout(() => {
47
- console.log("Starting to observe pagination target after delay");
48
37
  this.intersectionObserver.observe(target);
49
38
  }, 1000);
50
39
  }
51
40
 
52
41
  paginationTargetDisconnected(target) {
53
- console.log("Pagination target disconnected, stop observing...");
54
42
  this.intersectionObserver.unobserve(target);
55
43
  }
56
44
 
57
45
  disconnect() {
58
- console.log("Reverse infinite scroll controller disconnected");
59
46
  this.intersectionObserver.disconnect();
60
47
  }
61
48
 
@@ -67,15 +54,11 @@ export default class extends Controller {
67
54
 
68
55
  async loadOlder() {
69
56
  if (this.loadingValue) {
70
- console.log("Already loading, skipping...");
71
57
  return;
72
58
  }
73
59
 
74
- console.log("Loading older messages...");
75
- console.log("Current pageValue:", this.pageValue);
76
60
  this.loadingValue = true;
77
61
  const nextPage = this.pageValue + 1;
78
- console.log("Requesting page:", nextPage);
79
62
 
80
63
  // Store current scroll position
81
64
  const oldScrollHeight = this.element.scrollHeight;
@@ -83,7 +66,6 @@ export default class extends Controller {
83
66
 
84
67
  try {
85
68
  const url = this.buildUrl(nextPage);
86
- console.log("Fetching URL:", url);
87
69
 
88
70
  const response = await fetch(url, {
89
71
  headers: {
@@ -96,7 +78,6 @@ export default class extends Controller {
96
78
  }
97
79
 
98
80
  const html = await response.text();
99
- console.log("Received response for page", nextPage);
100
81
 
101
82
  // Parse the turbo-stream content
102
83
  const parser = new DOMParser();
@@ -112,7 +93,6 @@ export default class extends Controller {
112
93
  // Manually prepend the template content (for reverse infinite scroll)
113
94
  this.entriesTarget.insertAdjacentHTML("afterbegin", template.innerHTML);
114
95
  this.pageValue = nextPage;
115
- console.log(`Updated to page ${nextPage}`);
116
96
 
117
97
  // Maintain scroll position (prevent jumping to top)
118
98
  setTimeout(() => {
@@ -120,25 +100,13 @@ export default class extends Controller {
120
100
  const heightDifference = newScrollHeight - oldScrollHeight;
121
101
  this.element.scrollTop = oldScrollTop + heightDifference;
122
102
 
123
- console.log("Scroll position updated:", {
124
- oldScrollHeight,
125
- newScrollHeight,
126
- heightDifference,
127
- newScrollTop: this.element.scrollTop
128
- });
129
-
130
103
  // Set up observer for new pagination target if it exists
131
104
  if (this.hasPaginationTarget) {
132
- console.log("Found new pagination target, observing...");
133
105
  this.intersectionObserver.observe(this.paginationTarget);
134
- } else {
135
- console.log("No more pages to load");
136
106
  }
137
107
  }, 50);
138
108
  }
139
109
  } catch (error) {
140
- console.error("Error loading older messages:", error);
141
- console.log("Staying on page", this.pageValue, "due to error");
142
110
  } finally {
143
111
  this.loadingValue = false;
144
112
  }
@@ -9,7 +9,6 @@ export default class extends Controller {
9
9
  }
10
10
 
11
11
  connect() {
12
- console.log("Search controller initialized");
13
12
  // Find the search input
14
13
  this.searchInput = this.element.querySelector('input[type="search"]');
15
14
 
@@ -32,7 +32,6 @@ export default class extends Controller {
32
32
 
33
33
  handleVariableChange(event) {
34
34
  const input = event.target
35
- console.log(`Variable ${input.name} changed to:`, input.value)
36
35
 
37
36
  // Check if all required variables are filled and submit if so
38
37
  setTimeout(() => {
@@ -41,8 +40,6 @@ export default class extends Controller {
41
40
  }
42
41
 
43
42
  handleDateRangeChange(input, picker) {
44
- console.log(`Date range updated for ${input.name}:`, input.value)
45
-
46
43
  // Force update any related hidden fields
47
44
  this.updateRelatedDateFields(input, picker)
48
45
 
@@ -83,16 +80,10 @@ export default class extends Controller {
83
80
  // More robust empty check
84
81
  if (this.isEmpty(value)) {
85
82
  completed = false
86
- console.log(`Variable ${input.name} is empty:`, value)
87
- } else {
88
- console.log(`Variable ${input.name} has value:`, value)
89
83
  }
90
84
  })
91
85
 
92
- console.log(`Form completion check: ${completed ? 'Complete' : 'Incomplete'}`)
93
-
94
86
  if (completed) {
95
- console.log('Submitting form with all variables filled')
96
87
  form.submit()
97
88
  }
98
89
  }
@@ -112,11 +103,8 @@ export default class extends Controller {
112
103
 
113
104
  // Debug method to check current variable states
114
105
  debugVariables() {
115
- console.log("=== Current Variable States ===")
116
106
  const inputs = this.formTarget.querySelectorAll('input[name], select[name]')
117
107
  inputs.forEach(input => {
118
- console.log(`${input.name}: "${input.value}" (${typeof input.value})`)
119
108
  })
120
- console.log("===============================")
121
109
  }
122
- }
110
+ }
@@ -5,9 +5,10 @@ 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
7
  import VariablesController from "sage/controllers/variables_controller"
8
+ import QueryToggleController from "sage/controllers/query_toggle_controller"
8
9
 
9
10
  // Export all Sage controllers for manual registration
10
- export { SearchController, ClipboardController, SelectController, DashboardController, ReverseInfiniteScrollController, VariablesController }
11
+ export { SearchController, ClipboardController, SelectController, DashboardController, ReverseInfiniteScrollController, VariablesController, QueryToggleController }
11
12
 
12
13
  // Register all Sage controllers with the provided Stimulus application
13
14
  export function registerControllers(application) {
@@ -17,5 +18,6 @@ export function registerControllers(application) {
17
18
  application.register("sage--dashboard", DashboardController)
18
19
  application.register("sage--reverse-infinite-scroll", ReverseInfiniteScrollController)
19
20
  application.register("sage--variables", VariablesController)
21
+ application.register("sage--query-toggle", QueryToggleController)
20
22
  }
21
23
 
@@ -96,6 +96,8 @@ module Sage
96
96
  @query = Blazer::Query.new(query_params)
97
97
  @query.creator = blazer_user if @query.respond_to?(:creator)
98
98
  @query.status = "active" if @query.respond_to?(:status)
99
+ # Ensure data_source is set for SQL queries
100
+ @query.data_source ||= Blazer.data_sources.keys.first
99
101
 
100
102
  if @query.save
101
103
  redirect_to query_path(@query, params: variable_params(@query))
@@ -524,73 +526,3 @@ module Sage
524
526
  end
525
527
  end
526
528
  end
527
- #
528
- # module Sage
529
- # class QueriesController < ApplicationController
530
- # before_action :set_data_source
531
- #
532
- # def new
533
- # @query = Blazer::Query.new
534
- # end
535
- #
536
- # def create
537
- # @query = Blazer::Query.new(name: "Sage Query: #{Time.current}")
538
- # @query.statement = generate_sql_from_question(query_params[:question])
539
- # @query.creator = blazer_user if defined?(blazer_user)
540
- #
541
- # # Optionally save the query if configured
542
- # if Sage.configuration.auto_save_queries && @query.statement.present?
543
- # @query.save
544
- # end
545
- #
546
- # # Store the question for display
547
- # @question = query_params[:question]
548
- #
549
- # respond_to do |format|
550
- # format.turbo_stream
551
- # format.html { render :new }
552
- # end
553
- # end
554
- #
555
- # def run
556
- # @query = Blazer::Query.new(statement: query_params[:sql])
557
- #
558
- # # Use Blazer's run method to execute the query
559
- # @result = Blazer.data_sources[@data_source].run_statement(@query.statement)
560
- #
561
- # # Check for errors
562
- # if @result.error.present?
563
- # @error = @result.error
564
- # end
565
- #
566
- # respond_to do |format|
567
- # format.turbo_stream
568
- # format.html
569
- # end
570
- # end
571
- #
572
- # private
573
- #
574
- # def set_data_source
575
- # @data_source = params[:data_source] || Blazer.data_sources.keys.first
576
- # end
577
- #
578
- # def query_params
579
- # params.require(:query).permit(:question, :sql, :data_source)
580
- # end
581
- #
582
- # def generate_sql_from_question(question)
583
- # # Placeholder for AI integration
584
- # # In production, this would call your AI service (OpenAI, Anthropic, etc.)
585
- # "-- AI generated SQL for: #{question}\n" +
586
- # "-- TODO: Integrate with AI service\n" +
587
- # "SELECT 'Please configure AI service' as message;"
588
- # end
589
- #
590
- # def blazer_user
591
- # # Override this method to provide the current user
592
- # # For example: current_user.email if using Devise
593
- # "sage-user"
594
- # end
595
- # end
596
- # end
@@ -4,7 +4,6 @@ export default class extends Controller {
4
4
  static values = { text: String }
5
5
 
6
6
  connect() {
7
- console.log("Clipboard controller connected");
8
7
  }
9
8
 
10
9
  copy(event) {
@@ -6,15 +6,12 @@ export default class extends Controller {
6
6
  static values = { queries: Array };
7
7
 
8
8
  connect() {
9
- console.log("Dashboard controller connected");
10
- console.log("Queries value:", this.queriesValue);
11
9
 
12
10
  // Parse the queries value if it's a string
13
11
  const queriesData = typeof this.queriesValue === 'string' ?
14
12
  JSON.parse(this.queriesValue) : this.queriesValue;
15
13
  this.selectedQueries = Array.isArray(queriesData) ? [...queriesData] : [];
16
14
 
17
- console.log("Selected queries:", this.selectedQueries);
18
15
 
19
16
  this.render();
20
17
  this.setupSortable();
@@ -0,0 +1,24 @@
1
+ import { Controller } from "@hotwired/stimulus";
2
+
3
+ export default class extends Controller {
4
+ static targets = ["content", "icon"]
5
+
6
+ toggle() {
7
+ try {
8
+ const content = this.contentTarget
9
+ const icon = this.iconTarget
10
+
11
+ const isHidden = window.getComputedStyle(content).display === "none"
12
+
13
+ if (isHidden) {
14
+ content.style.display = "block"
15
+ icon.textContent = "visibility_off"
16
+ } else {
17
+ content.style.display = "none"
18
+ icon.textContent = "visibility"
19
+ }
20
+ } catch (error) {
21
+ console.error("Error in toggle:", error)
22
+ }
23
+ }
24
+ }
@@ -1,6 +1,5 @@
1
1
  import { Controller } from "@hotwired/stimulus";
2
2
 
3
- console.log("Reverse infinite scroll controller file loaded!");
4
3
 
5
4
  export default class extends Controller {
6
5
  static targets = ["entries", "pagination"];
@@ -11,12 +10,10 @@ export default class extends Controller {
11
10
  };
12
11
 
13
12
  initialize() {
14
- console.log("Reverse infinite scroll controller initialized");
15
13
  this.intersectionObserver = new IntersectionObserver(
16
14
  (entries) => {
17
15
  entries.forEach((entry) => {
18
16
  if (entry.isIntersecting) {
19
- console.log("Pagination target is visible - loading older messages");
20
17
  this.loadOlder();
21
18
  }
22
19
  });
@@ -29,33 +26,25 @@ export default class extends Controller {
29
26
  }
30
27
 
31
28
  connect() {
32
- console.log("Reverse infinite scroll controller connected");
33
- console.log("Initial page value:", this.pageValue);
34
29
 
35
30
  if (this.hasPaginationTarget) {
36
- console.log("Found pagination target, observing...");
37
31
  this.intersectionObserver.observe(this.paginationTarget);
38
32
  } else {
39
- console.log("No pagination target found");
40
33
  }
41
34
  }
42
35
 
43
36
  paginationTargetConnected(target) {
44
- console.log("Pagination target connected dynamically, observing...");
45
37
  // Add a delay to allow scroll-to-bottom to happen first
46
38
  setTimeout(() => {
47
- console.log("Starting to observe pagination target after delay");
48
39
  this.intersectionObserver.observe(target);
49
40
  }, 1000);
50
41
  }
51
42
 
52
43
  paginationTargetDisconnected(target) {
53
- console.log("Pagination target disconnected, stop observing...");
54
44
  this.intersectionObserver.unobserve(target);
55
45
  }
56
46
 
57
47
  disconnect() {
58
- console.log("Reverse infinite scroll controller disconnected");
59
48
  this.intersectionObserver.disconnect();
60
49
  }
61
50
 
@@ -67,15 +56,11 @@ export default class extends Controller {
67
56
 
68
57
  async loadOlder() {
69
58
  if (this.loadingValue) {
70
- console.log("Already loading, skipping...");
71
59
  return;
72
60
  }
73
61
 
74
- console.log("Loading older messages...");
75
- console.log("Current pageValue:", this.pageValue);
76
62
  this.loadingValue = true;
77
63
  const nextPage = this.pageValue + 1;
78
- console.log("Requesting page:", nextPage);
79
64
 
80
65
  // Store current scroll position
81
66
  const oldScrollHeight = this.element.scrollHeight;
@@ -83,7 +68,6 @@ export default class extends Controller {
83
68
 
84
69
  try {
85
70
  const url = this.buildUrl(nextPage);
86
- console.log("Fetching URL:", url);
87
71
 
88
72
  const response = await fetch(url, {
89
73
  headers: {
@@ -96,7 +80,6 @@ export default class extends Controller {
96
80
  }
97
81
 
98
82
  const html = await response.text();
99
- console.log("Received response for page", nextPage);
100
83
 
101
84
  // Parse the turbo-stream content
102
85
  const parser = new DOMParser();
@@ -112,7 +95,6 @@ export default class extends Controller {
112
95
  // Manually prepend the template content (for reverse infinite scroll)
113
96
  this.entriesTarget.insertAdjacentHTML("afterbegin", template.innerHTML);
114
97
  this.pageValue = nextPage;
115
- console.log(`Updated to page ${nextPage}`);
116
98
 
117
99
  // Maintain scroll position (prevent jumping to top)
118
100
  setTimeout(() => {
@@ -120,25 +102,16 @@ export default class extends Controller {
120
102
  const heightDifference = newScrollHeight - oldScrollHeight;
121
103
  this.element.scrollTop = oldScrollTop + heightDifference;
122
104
 
123
- console.log("Scroll position updated:", {
124
- oldScrollHeight,
125
- newScrollHeight,
126
- heightDifference,
127
- newScrollTop: this.element.scrollTop
128
- });
129
105
 
130
106
  // Set up observer for new pagination target if it exists
131
107
  if (this.hasPaginationTarget) {
132
- console.log("Found new pagination target, observing...");
133
108
  this.intersectionObserver.observe(this.paginationTarget);
134
109
  } else {
135
- console.log("No more pages to load");
136
110
  }
137
111
  }, 50);
138
112
  }
139
113
  } catch (error) {
140
114
  console.error("Error loading older messages:", error);
141
- console.log("Staying on page", this.pageValue, "due to error");
142
115
  } finally {
143
116
  this.loadingValue = false;
144
117
  }
@@ -9,7 +9,6 @@ export default class extends Controller {
9
9
  }
10
10
 
11
11
  connect() {
12
- console.log("Search controller initialized");
13
12
  // Find the search input
14
13
  this.searchInput = this.element.querySelector('input[type="search"]');
15
14
 
@@ -32,7 +32,6 @@ export default class extends Controller {
32
32
 
33
33
  handleVariableChange(event) {
34
34
  const input = event.target
35
- console.log(`Variable ${input.name} changed to:`, input.value)
36
35
 
37
36
  // Check if all required variables are filled and submit if so
38
37
  setTimeout(() => {
@@ -41,7 +40,6 @@ export default class extends Controller {
41
40
  }
42
41
 
43
42
  handleDateRangeChange(input, picker) {
44
- console.log(`Date range updated for ${input.name}:`, input.value)
45
43
 
46
44
  // Force update any related hidden fields
47
45
  this.updateRelatedDateFields(input, picker)
@@ -83,16 +81,12 @@ export default class extends Controller {
83
81
  // More robust empty check
84
82
  if (this.isEmpty(value)) {
85
83
  completed = false
86
- console.log(`Variable ${input.name} is empty:`, value)
87
84
  } else {
88
- console.log(`Variable ${input.name} has value:`, value)
89
85
  }
90
86
  })
91
87
 
92
- console.log(`Form completion check: ${completed ? 'Complete' : 'Incomplete'}`)
93
88
 
94
89
  if (completed) {
95
- console.log('Submitting form with all variables filled')
96
90
  form.submit()
97
91
  }
98
92
  }
@@ -112,11 +106,6 @@ export default class extends Controller {
112
106
 
113
107
  // Debug method to check current variable states
114
108
  debugVariables() {
115
- console.log("=== Current Variable States ===")
116
109
  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
110
  }
122
111
  }
@@ -5,9 +5,10 @@ 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
7
  import VariablesController from "sage/controllers/variables_controller"
8
+ import QueryToggleController from "sage/controllers/query_toggle_controller.js"
8
9
 
9
10
  // Export all Sage controllers for manual registration
10
- export { SearchController, ClipboardController, SelectController, DashboardController, ReverseInfiniteScrollController, VariablesController }
11
+ export { SearchController, ClipboardController, SelectController, DashboardController, ReverseInfiniteScrollController, VariablesController, QueryToggleController }
11
12
 
12
13
  // Register all Sage controllers with the provided Stimulus application
13
14
  export function registerControllers(application) {
@@ -17,5 +18,6 @@ export function registerControllers(application) {
17
18
  application.register("sage--dashboard", DashboardController)
18
19
  application.register("sage--reverse-infinite-scroll", ReverseInfiniteScrollController)
19
20
  application.register("sage--variables", VariablesController)
21
+ application.register("sage--query-toggle", QueryToggleController)
20
22
  }
21
23
 
@@ -9,36 +9,40 @@
9
9
  return moment.tz(time.format(format), timeZone)
10
10
  }
11
11
  <% end %>
12
- <div data-controller="sage--variables" data-sage--variables-form-target="form">
13
- <form id="bind" method="get" action="<%= action %>" class="form-inline" style="margin-bottom: 15px;">
14
- <% date_vars = ["start_time", "end_time"] %>
15
- <% if (date_vars - @bind_vars).empty? %>
16
- <% @bind_vars = @bind_vars - date_vars %>
17
- <% else %>
18
- <% date_vars = nil %>
19
- <% end %>
20
-
21
- <% @bind_vars.each_with_index do |var, i| %>
22
- <div style="margin-bottom: 10px;">
23
- <div style="margin-bottom: 3px;">
24
- <small class="text-muted"><code>{{<%= var %>}}</code></small>
25
- </div>
26
- </div>
27
- <% if (data = @smart_vars[var]) %>
28
- <%= select_tag var, options_for_select([[nil, nil]] + data, selected: var_params[var]), style: "margin-right: 20px; width: 200px; display: none;", data: { "sage--variables-target": "variable" } %>
29
- <%= javascript_tag nonce: true do %>
30
- $("#<%= var %>").selectize({
31
- create: true
32
- });
12
+
13
+ <!-- Filter Navigation Area -->
14
+ <div class="surface border left-round" style="padding: 16px; margin-bottom: 20px; background-color: #f8f9fa;">
15
+ <h6 style="margin: 0 0 16px 0; color: #6b7280; font-weight: 500;">Query Filters</h6>
16
+
17
+ <div data-controller="sage--variables" data-sage--variables-form-target="form">
18
+ <form id="bind" method="get" action="<%= action %>">
19
+ <div class="grid">
20
+ <% date_vars = ["start_time", "end_time"] %>
21
+ <% if (date_vars - @bind_vars).empty? %>
22
+ <% @bind_vars = @bind_vars - date_vars %>
23
+ <% else %>
24
+ <% date_vars = nil %>
33
25
  <% end %>
34
- <% elsif var.end_with?("_at") || var == "start_time" || var == "end_time" %>
35
- <%= hidden_field_tag var, var_params[var], data: { "sage--variables-target": "variable" } %>
36
26
 
37
- <div class="selectize-control single" style="width: 200px;">
38
- <div id="<%= var %>-select" class="selectize-input" style="display: inline-block;">
39
- <span>Select a date</span>
40
- </div>
41
- </div>
27
+ <% @bind_vars.each_with_index do |var, i| %>
28
+ <div class="s12 m6 l4" style="margin-bottom: 20px;">
29
+ <label style="display: block; margin-bottom: 6px; color: #6b7280; font-size: 12px; font-weight: 500; text-transform: uppercase; letter-spacing: 0.5px;">
30
+ {<%= var %>}
31
+ </label>
32
+ <% if (data = @smart_vars[var]) %>
33
+ <%= select_tag var, options_for_select([[nil, nil]] + data, selected: var_params[var]),
34
+ style: "width: 100%; padding: 10px; border: 1px solid #d1d5db; border-radius: 6px; font-size: 14px; height: 42px; box-sizing: border-box;",
35
+ data: { "sage--variables-target": "variable" } %>
36
+ <%= javascript_tag nonce: true do %>
37
+ $("#<%= var %>").selectize({
38
+ create: true
39
+ });
40
+ <% end %>
41
+ <% elsif var.end_with?("_at") || var == "start_time" || var == "end_time" %>
42
+ <%= hidden_field_tag var, var_params[var], data: { "sage--variables-target": "variable" } %>
43
+ <div id="<%= var %>-select" style="width: 100%; padding: 10px; border: 1px solid #d1d5db; border-radius: 6px; background: white; cursor: pointer; font-size: 14px; color: #6b7280; height: 42px; box-sizing: border-box; display: flex; align-items: center;">
44
+ <span>Select a date</span>
45
+ </div>
42
46
 
43
47
  <%= javascript_tag nonce: true do %>
44
48
  (function() {
@@ -71,24 +75,41 @@
71
75
  }
72
76
  })()
73
77
  <% end %>
74
- <% else %>
75
- <%= text_field_tag var, var_params[var], style: "width: 120px; margin-right: 20px;", autofocus: i == 0 && !var.end_with?("_at") && !var_params[var], class: "form-control", data: { "sage--variables-target": "variable" } %>
76
- <% end %>
77
- <% end %>
78
-
79
- <% if date_vars %>
80
- <% date_vars.each do |var| %>
81
- <%= hidden_field_tag var, var_params[var], data: { "sage--variables-target": "variable" } %>
82
- <% end %>
78
+ <% else %>
79
+ <%= text_field_tag var, var_params[var],
80
+ style: "width: 100%; padding: 10px; border: 1px solid #d1d5db; border-radius: 6px; font-size: 14px; height: 42px; box-sizing: border-box;",
81
+ autofocus: i == 0 && !var.end_with?("_at") && !var_params[var],
82
+ data: { "sage--variables-target": "variable" },
83
+ placeholder: "Enter #{var.humanize.downcase}" %>
84
+ <% end %>
85
+ </div>
86
+ <% end %>
83
87
 
84
- <div style="margin-bottom: 5px; text-align: left;">
85
- <small class="text-muted"><code>{{<%= date_vars.join('}}, {{') %>}}</code></small>
88
+ <% if date_vars %>
89
+ <% date_vars.each do |var| %>
90
+ <%= hidden_field_tag var, var_params[var], data: { "sage--variables-target": "variable" } %>
91
+ <% end %>
92
+
93
+ <div class="s12 m6 l4" style="margin-bottom: 20px;">
94
+ <label style="display: block; margin-bottom: 6px; color: #6b7280; font-size: 12px; font-weight: 500; text-transform: uppercase; letter-spacing: 0.5px;">
95
+ {<%= date_vars.join('}, {') %>}
96
+ </label>
97
+ <div id="reportrange" style="width: 100%; padding: 10px; border: 1px solid #d1d5db; border-radius: 6px; background: white; cursor: pointer; font-size: 14px; color: #6b7280; height: 42px; box-sizing: border-box; display: flex; align-items: center;">
98
+ <span>Select a time range</span>
99
+ </div>
100
+ </div>
101
+ <% end %>
86
102
  </div>
87
- <div class="selectize-control single" style="width: 300px;">
88
- <div id="reportrange" class="selectize-input" style="display: inline-block;">
89
- <span>Select a time range</span>
90
- </div>
103
+
104
+ <div class="right-align" style="margin-top: 16px;">
105
+ <button type='submit' class='button primary'>
106
+ <i>play_arrow</i>
107
+ Run Query
108
+ </button>
91
109
  </div>
110
+ </form>
111
+ </div>
112
+ </div>
92
113
 
93
114
  <%= javascript_tag nonce: true do %>
94
115
  function dateStr(daysAgo) {
@@ -160,9 +181,4 @@
160
181
  }
161
182
  }
162
183
  <% end %>
163
- <% end %>
164
-
165
- <button type='submit' class='btn btn-primary'>Run</button>
166
- </form>
167
- </div>
168
184
  <% end %>
@@ -40,7 +40,7 @@
40
40
  <%= link_to dashboard.name, dashboard_path(dashboard), class: "text-decoration-none" %>
41
41
  </td>
42
42
  <% if Blazer.user_class %>
43
- <td><%= dashboard.creator.name %></td>
43
+ <td><%= dashboard.creator&.name %></td>
44
44
  <% end %>
45
45
  </tr>
46
46
  <% end %>
@@ -11,7 +11,7 @@
11
11
  <% @variable_params = @query.persisted? ? variable_params(@query) : nested_variable_params(@query) %>
12
12
 
13
13
  <%= form_for @query, url: (@query.persisted? ? query_path(@query, params: @variable_params) : queries_path(params: @variable_params)), html: {autocomplete: "off", class: ""} do |f| %>
14
- <div class="grid">
14
+ <div class="grid" style='margin-top: 0px'>
15
15
  <div class="s10">
16
16
  <div class="field label border">
17
17
  <%= f.text_field :name %>
@@ -19,7 +19,7 @@
19
19
  </div>
20
20
 
21
21
  <div class="field textarea label border">
22
- <%= f.text_area :description, style: "height: 80px; resize: vertical;" %>
22
+ <%= f.text_area :description, style: "height: 145px; resize: vertical;" %>
23
23
  <%= f.label :description, "Description (Optional)", class: (@query.description.present? ? "active" : "") %>
24
24
  </div>
25
25
 
@@ -28,14 +28,28 @@
28
28
 
29
29
  <div class="s2">
30
30
  <nav style="display: flex; flex-direction: column; gap: 12px; padding-left: 8px;">
31
+ <button type='button' onclick="formatSQL()" class="button secondary" title="Format SQL">
32
+ <i>auto_fix_high</i>
33
+ </button>
34
+
35
+ <%= form_with url: (defined?(sage) && sage.respond_to?(:run_queries_path) ? sage.run_queries_path : run_queries_path), method: :post, local: false, style: "margin: 0;" do |f| %>
36
+ <%= f.hidden_field :statement, id: 'run_query_statement' %>
37
+ <%= f.hidden_field :query_id, value: @query.id %>
38
+ <%= f.hidden_field :data_source, value: @query.data_source || Blazer.data_sources.keys.first %>
39
+ <button type='submit' class="button primary" title="Run Query">
40
+ <i>play_arrow</i>
41
+ </button>
42
+ <% end %>
43
+
44
+ <button type="submit" name="commit" value="<%= @query.persisted? ? "Update" : "Create" %>" class="button primary" title="<%= @query.persisted? ? "Update" : "Create" %>">
45
+ <i><%= @query.persisted? ? "save" : "add" %></i>
46
+ </button>
47
+
31
48
  <% if @query.persisted? %>
32
49
  <%= link_to query_path(@query), method: :delete, "data-confirm" => "Are you sure?", class: "button red", title: "Delete" do %>
33
50
  <i>delete</i>
34
51
  <% end %>
35
52
  <% end %>
36
- <button type="submit" name="commit" value="<%= @query.persisted? ? "Update" : "Create" %>" class="button primary" title="<%= @query.persisted? ? "Update" : "Create" %>">
37
- <i><%= @query.persisted? ? "save" : "add" %></i>
38
- </button>
39
53
  </nav>
40
54
  </div>
41
55
  </div>
@@ -28,7 +28,7 @@
28
28
  }
29
29
  </style>
30
30
 
31
- <div id=<%= dom_id(query, 'statement-box') %> class='' style="position: relative;">
31
+ <div id=<%= dom_id(query, 'statement-box') %> class='' style="position: relative; border-radius: 10px;">
32
32
  <%
33
33
  # Handle different contexts - job vs regular view rendering
34
34
  form_url = if local_assigns[:form_url]
@@ -46,12 +46,8 @@
46
46
  <%= f.hidden_field :statement, id: 'query_statement' %>
47
47
  <%= f.hidden_field :query_id, value: query.id %>
48
48
  <%= f.hidden_field :data_source, value: query.data_source || Blazer.data_sources.keys.first %>
49
- <div id="editor-container" style="position: relative; overflow: hidden;">
50
- <div id="editor" style="height: 200px;" data-initial-content="<%= html_escape(local_assigns[:statement] || query.statement) %>"><%= local_assigns[:statement] || query.statement %></div>
51
- <div style="position: absolute; bottom: 10px; right: 15px; z-index: 1000; border-radius: 4px; padding: 5px; display: flex; gap: 8px;">
52
- <button type='button' onclick="formatSQL()" class="secondary">Format</button>
53
- <button type='submit' class="<%= 'glow-button' if local_assigns[:statement].present? %>">Run</button>
54
- </div>
49
+ <div id="editor-container" style="position: relative;">
50
+ <div id="editor" style="height: 225px;" data-initial-content="<%= html_escape(local_assigns[:statement] || query.statement) %>"><%= local_assigns[:statement] || query.statement %></div>
55
51
  </div>
56
52
  <% end %>
57
53
  </div>
@@ -79,7 +75,7 @@
79
75
  'COALESCE', 'NULLIF', 'DATE_TRUNC', 'EXTRACT', 'SUBSTRING',
80
76
  'CONCAT', 'LENGTH', 'LOWER', 'UPPER', 'TRIM', 'REPLACE'];
81
77
 
82
- // Preserve string literals and comments
78
+ // Preserve string literals, comments, and JSON operators
83
79
  const preservedStrings = [];
84
80
  let preservedIndex = 0;
85
81
 
@@ -100,6 +96,12 @@
100
96
  return `__PRESERVED_STRING_${preservedIndex++}__`;
101
97
  });
102
98
 
99
+ // Preserve JSON operators and comparison operators EARLY before any other processing
100
+ sql = sql.replace(/->>|->|#>>|#>|@>|<@|>=|<=|<>|!=|\?\?|\?&|\?/g, (match) => {
101
+ preservedStrings.push(match);
102
+ return `__PRESERVED_STRING_${preservedIndex++}__`;
103
+ });
104
+
103
105
  // Remove extra whitespace and normalize spaces
104
106
  sql = sql.replace(/\s+/g, ' ').trim();
105
107
 
@@ -154,15 +156,25 @@
154
156
  // Remove leading newline if present
155
157
  sql = sql.replace(/^\n+/, '');
156
158
 
157
- // Ensure consistent spacing around operators
159
+ // Ensure consistent spacing around basic operators (complex operators already preserved)
158
160
  sql = sql.replace(/\s*=\s*/g, ' = ');
159
- sql = sql.replace(/\s*<>\s*/g, ' <> ');
160
- sql = sql.replace(/\s*!=\s*/g, ' != ');
161
- sql = sql.replace(/\s*<=\s*/g, ' <= ');
162
- sql = sql.replace(/\s*>=\s*/g, ' >= ');
163
161
  sql = sql.replace(/\s*<\s*/g, ' < ');
164
162
  sql = sql.replace(/\s*>\s*/g, ' > ');
165
163
 
164
+ // Final fix: repair any broken operators that slipped through
165
+ sql = sql.replace(/>\s*=/g, '>=');
166
+ sql = sql.replace(/<\s*=/g, '<=');
167
+ sql = sql.replace(/@\s*>/g, '@>');
168
+ sql = sql.replace(/<\s*>/g, '<>');
169
+ sql = sql.replace(/!\s*=/g, '!=');
170
+ sql = sql.replace(/-\s*>/g, '->');
171
+ sql = sql.replace(/-\s*>>/g, '->>');
172
+ sql = sql.replace(/#\s*>/g, '#>');
173
+ sql = sql.replace(/#\s*>>/g, '#>>');
174
+ sql = sql.replace(/<\s*@/g, '<@');
175
+ sql = sql.replace(/\?\s*\?/g, '??');
176
+ sql = sql.replace(/\?\s*&/g, '?&');
177
+
166
178
  return sql;
167
179
  }
168
180
 
@@ -182,7 +194,8 @@
182
194
  enableLiveAutocompletion: false,
183
195
  highlightActiveLine: false,
184
196
  fontSize: 12,
185
- minLines: 10
197
+ minLines: 10,
198
+ scrollPastEnd: 0.25
186
199
  })
187
200
  editor.renderer.setShowGutter(true)
188
201
  editor.renderer.setPrintMarginColumn(false)
@@ -190,15 +203,22 @@
190
203
  editor.renderer.setPadding(10)
191
204
  editor.getSession().setUseWrapMode(true)
192
205
 
193
- // Ensure scrolling works properly with bottom margin
194
- editor.renderer.setScrollMargin(0, 0, 50, 0) // top, right, bottom, left margins
206
+ // Ensure proper scrolling with bottom margin for buttons
207
+ editor.renderer.setScrollMargin(0, 0, 60, 0) // top, right, bottom, left margins
195
208
  editor.setAutoScrollEditorIntoView(true)
209
+
210
+ // Force scrollbar to always be visible for consistent layout
211
+ editor.setOption("vScrollBarAlwaysVisible", true)
212
+
213
+ // Add bottom padding to content area to ensure last line is fully visible
214
+ editor.renderer.setPadding(10, 10, 60, 10) // top, right, bottom, left padding
196
215
 
197
- // Update both hidden fields when editor content changes
216
+ // Update all hidden fields when editor content changes
198
217
  editor.getSession().on("change", function () {
199
218
  const value = editor.getValue()
200
- $("#query_statement").val(value) // For run form
219
+ $("#query_statement").val(value) // For statement_box run form
201
220
  $("#query_statement_form").val(value) // For main form
221
+ $("#run_query_statement").val(value) // For new_form run button
202
222
  })
203
223
 
204
224
  // Set initial value from data attribute or current content
@@ -212,6 +232,7 @@
212
232
  const initialValue = editor.getValue()
213
233
  $("#query_statement").val(initialValue)
214
234
  $("#query_statement_form").val(initialValue)
235
+ $("#run_query_statement").val(initialValue)
215
236
  editor.focus()
216
237
 
217
238
  // Add top padding to ace_scroller and gutter to keep them aligned
@@ -27,7 +27,6 @@
27
27
  setTimeout(() => {
28
28
  const messagesDiv = document.getElementById('<%= dom_id(@query, 'messages') %>');
29
29
  if (messagesDiv) {
30
- console.log("Scrolling to bottom, scrollHeight:", messagesDiv.scrollHeight);
31
30
  messagesDiv.scrollTop = messagesDiv.scrollHeight;
32
31
  }
33
32
  }, 100);
@@ -35,7 +34,6 @@
35
34
  setTimeout(() => {
36
35
  const messagesDiv = document.getElementById('<%= dom_id(@query, 'messages') %>');
37
36
  if (messagesDiv) {
38
- console.log("Final scroll to bottom, scrollHeight:", messagesDiv.scrollHeight);
39
37
  messagesDiv.scrollTop = messagesDiv.scrollHeight;
40
38
  }
41
39
  }, 500);
@@ -1,36 +1,67 @@
1
1
  <%= blazer_title "New Query" %>
2
- <%# <%= render partial: "form" %>
3
2
 
4
3
  <div class="padding left-align">
5
- <h5 class="">New Query</h5>
4
+ <h5>New Query</h5>
6
5
  </div>
7
6
 
8
7
  <div class="sage-container">
9
- <div class="sage-header">
10
- <h5>What are you curious about?</h5>
11
- </div>
12
-
13
- <%= form_with url: queries_path, method: :post, local: true, scope: :query do |f| %>
14
- <div class="form-group">
15
- <%= label_tag "query[question]", "Your Question", class: "form-label" %>
16
- <%= text_area_tag "query[question]", nil,
17
- class: "form-control",
18
- rows: 8,
19
- placeholder: "e.g., Show me the top 10 customers by revenue in the last 30 days",
20
- required: true,
21
- data: { sage_target: "question" } %>
22
- <small class="form-text text-muted">
23
- Be specific about tables, columns, and conditions when possible
24
- </small>
8
+ <!-- BeerCSS Tabs -->
9
+ <div>
10
+ <div class="tabs">
11
+ <a id="prompt-tab" class="active">Ask AI</a>
12
+ <a id="sql-tab">Write SQL</a>
25
13
  </div>
14
+
15
+ <!-- Prompt Mode Page -->
16
+ <div id="prompt-page" class="page padding active">
17
+ <%= form_with url: queries_path, method: :post, local: true, scope: :query do |f| %>
18
+ <div class="form-group">
19
+ <%= f.label :question, "Your Question", class: "form-label left-align" %>
20
+ <%= f.text_area :question,
21
+ class: "form-control",
22
+ rows: 8,
23
+ placeholder: "e.g., Show me the top 10 customers by revenue in the last 30 days" %>
24
+ <small class="form-text text-muted">
25
+ Be specific about tables, columns, and conditions when possible
26
+ </small>
27
+ </div>
26
28
 
27
- <div class="form-actions">
28
- <button type='submit' data-disable-with='Generating...'>Generate Report</button>
29
+ <div class="form-actions">
30
+ <button type='submit' data-disable-with='Generating...'>Generate Report</button>
31
+ </div>
32
+ <% end %>
29
33
  </div>
30
- <% end %>
31
34
 
32
- <turbo-frame id="query_result">
33
- </turbo-frame>
35
+ <!-- SQL Mode Page -->
36
+ <div id="sql-page" class="page padding">
37
+ <%= form_with url: queries_path, method: :post, local: true, scope: :query do |f| %>
38
+ <div class="form-group">
39
+ <%= f.label :name, "Query Name", class: "form-label left-align" %>
40
+ <%= f.text_field :name,
41
+ class: "form-control",
42
+ placeholder: "Enter a name for your query",
43
+ value: params[:name] %>
44
+ </div>
45
+
46
+ <div class="form-group">
47
+ <%= f.label :statement, "SQL Statement", class: "form-label left-align" %>
48
+ <%
49
+ forked_statement = ""
50
+ if params[:fork_query_id].present?
51
+ forked_query = Blazer::Query.find_by(id: params[:fork_query_id])
52
+ forked_statement = forked_query&.statement || ""
53
+ end
54
+ %>
55
+ <%= f.hidden_field :statement, id: 'sql_statement_field' %>
56
+ <div id="sql-editor" style="height: 300px; border: 1px solid #ddd; border-radius: 4px;" data-initial-content="<%= html_escape(forked_statement) %>"><%= forked_statement %></div>
57
+ </div>
58
+
59
+ <div class="form-actions">
60
+ <button type='submit' data-disable-with='Creating...'>Create Query</button>
61
+ </div>
62
+ <% end %>
63
+ </div>
64
+ </div>
34
65
 
35
66
  <% if @schema.present? %>
36
67
  <div class="padding">
@@ -75,15 +106,6 @@
75
106
  padding: 20px;
76
107
  }
77
108
 
78
- .sage-header {
79
- margin-bottom: 30px;
80
- text-align: center;
81
- }
82
-
83
- .sage-header h1 {
84
- margin-bottom: 10px;
85
- }
86
-
87
109
  .form-group {
88
110
  margin-bottom: 20px;
89
111
  }
@@ -94,6 +116,10 @@
94
116
  display: block;
95
117
  }
96
118
 
119
+ .form-label.left-align {
120
+ text-align: left;
121
+ }
122
+
97
123
  .form-control {
98
124
  width: 100%;
99
125
  padding: 8px 12px;
@@ -130,33 +156,6 @@
130
156
  text-align: center;
131
157
  }
132
158
 
133
- .btn {
134
- display: inline-block;
135
- padding: 10px 20px;
136
- font-size: 16px;
137
- font-weight: 500;
138
- text-align: center;
139
- text-decoration: none;
140
- border: none;
141
- border-radius: 4px;
142
- cursor: pointer;
143
- transition: all 0.2s;
144
- }
145
-
146
- .btn-primary {
147
- background-color: #4CAF50;
148
- color: white;
149
- }
150
-
151
- .btn-primary:hover {
152
- background-color: #45a049;
153
- }
154
-
155
- .btn-primary:disabled {
156
- opacity: 0.6;
157
- cursor: not-allowed;
158
- }
159
-
160
159
  .text-muted {
161
160
  color: #666;
162
161
  font-size: 14px;
@@ -190,6 +189,135 @@
190
189
  text-overflow: ellipsis;
191
190
  margin-bottom: 10px;
192
191
  }
193
-
194
192
  </style>
195
193
 
194
+ <script>
195
+ // ACE Editor initialization
196
+ let sqlEditor = null;
197
+
198
+ function initializeAceEditor() {
199
+ if (sqlEditor) {
200
+ sqlEditor.destroy();
201
+ }
202
+
203
+ const editorDiv = document.getElementById('sql-editor');
204
+ if (!editorDiv) return;
205
+
206
+ sqlEditor = ace.edit("sql-editor");
207
+ sqlEditor.setTheme("ace/theme/twilight");
208
+ sqlEditor.getSession().setMode("ace/mode/sql");
209
+ sqlEditor.setOptions({
210
+ enableBasicAutocompletion: false,
211
+ enableSnippets: false,
212
+ enableLiveAutocompletion: false,
213
+ highlightActiveLine: false,
214
+ fontSize: 12,
215
+ minLines: 10,
216
+ scrollPastEnd: 0.25
217
+ });
218
+ sqlEditor.renderer.setShowGutter(true);
219
+ sqlEditor.renderer.setPrintMarginColumn(false);
220
+ sqlEditor.setShowPrintMargin(false);
221
+ sqlEditor.renderer.setPadding(10);
222
+ sqlEditor.getSession().setUseWrapMode(true);
223
+
224
+ // Set initial content
225
+ const initialContent = editorDiv.getAttribute("data-initial-content") || "";
226
+ if (initialContent) {
227
+ sqlEditor.setValue(initialContent, -1);
228
+ }
229
+
230
+ // Update hidden field when content changes
231
+ sqlEditor.getSession().on("change", function() {
232
+ const statementField = document.getElementById('sql_statement_field');
233
+ if (statementField) {
234
+ statementField.value = sqlEditor.getValue();
235
+ }
236
+ });
237
+
238
+ // Set initial value in hidden field
239
+ const statementField = document.getElementById('sql_statement_field');
240
+ if (statementField) {
241
+ statementField.value = sqlEditor.getValue();
242
+ }
243
+ }
244
+
245
+ document.addEventListener('DOMContentLoaded', function() {
246
+ const promptTab = document.getElementById('prompt-tab');
247
+ const sqlTab = document.getElementById('sql-tab');
248
+ const promptPage = document.getElementById('prompt-page');
249
+ const sqlPage = document.getElementById('sql-page');
250
+
251
+ function switchToPromptMode() {
252
+ promptTab.classList.add('active');
253
+ sqlTab.classList.remove('active');
254
+ promptPage.classList.add('active');
255
+ sqlPage.classList.remove('active');
256
+ }
257
+
258
+ function switchToSqlMode() {
259
+ sqlTab.classList.add('active');
260
+ promptTab.classList.remove('active');
261
+ sqlPage.classList.add('active');
262
+ promptPage.classList.remove('active');
263
+ }
264
+
265
+ promptTab.addEventListener('click', switchToPromptMode);
266
+ sqlTab.addEventListener('click', switchToSqlMode);
267
+
268
+ // If we have fork parameters, automatically switch to SQL mode
269
+ const urlParams = new URLSearchParams(window.location.search);
270
+ if (urlParams.get('fork_query_id') || urlParams.get('name')) {
271
+ console.log('Fork detected, switching to SQL mode');
272
+ switchToSqlMode();
273
+ // Initialize ACE editor immediately for forks
274
+ setTimeout(() => {
275
+ console.log('Initializing ACE editor for fork');
276
+ initializeAceEditor();
277
+ }, 200);
278
+ }
279
+ });
280
+
281
+ // Initialize ACE editor when SQL tab is clicked
282
+ document.addEventListener('click', function(e) {
283
+ if (e.target.id === 'sql-tab') {
284
+ setTimeout(() => {
285
+ initializeAceEditor();
286
+ }, 100);
287
+ }
288
+ });
289
+
290
+ // Ensure form submission captures the editor value
291
+ document.addEventListener('submit', function(e) {
292
+ if (e.target.closest('#sql-page') && sqlEditor) {
293
+ const statementField = document.getElementById('sql_statement_field');
294
+ if (statementField) {
295
+ statementField.value = sqlEditor.getValue();
296
+ }
297
+ }
298
+ });
299
+
300
+ // Handle Turbo navigation for fork detection
301
+ document.addEventListener('turbo:load', function() {
302
+ const urlParams = new URLSearchParams(window.location.search);
303
+ if (urlParams.get('fork_query_id') || urlParams.get('name')) {
304
+ console.log('Fork detected via turbo:load, switching to SQL mode');
305
+ const promptTab = document.getElementById('prompt-tab');
306
+ const sqlTab = document.getElementById('sql-tab');
307
+ const promptPage = document.getElementById('prompt-page');
308
+ const sqlPage = document.getElementById('sql-page');
309
+
310
+ if (sqlTab && promptTab && sqlPage && promptPage) {
311
+ sqlTab.classList.add('active');
312
+ promptTab.classList.remove('active');
313
+ sqlPage.classList.add('active');
314
+ promptPage.classList.remove('active');
315
+
316
+ setTimeout(() => {
317
+ console.log('Initializing ACE editor for fork via turbo:load');
318
+ initializeAceEditor();
319
+ }, 200);
320
+ }
321
+ }
322
+ });
323
+ </script>
@@ -6,7 +6,7 @@
6
6
  </h5>
7
7
  </div>
8
8
 
9
- <div class="row right-align">
9
+ <div class="row right-align" style="margin-bottom: 24px;">
10
10
  <%= link_to "Edit", edit_query_path(@query, params: variable_params(@query)), class: "btn btn-default", data: { turbo: false }, disabled: !@query.editable?(blazer_user) %>
11
11
  <%= link_to "Fork", new_query_path(params: {variables: variable_params(@query), fork_query_id: @query.id, data_source: @query.data_source, name: @query.name}), class: "btn btn-info" %>
12
12
  <% if !@error && @success %>
@@ -30,7 +30,14 @@
30
30
 
31
31
  <%= render partial: "sage/variables", locals: { action: query_path(@query) } %>
32
32
 
33
- <pre id="code"><code><%= @statement.display_statement %></code></pre>
33
+ <div data-controller="sage--query-toggle">
34
+ <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
35
+ <span>Statement</span>
36
+ <i class="material-icons" data-sage--query-toggle-target="icon" data-action="click->sage--query-toggle#toggle" style="cursor: pointer; font-size: 18px; opacity: 0.6;">visibility_off</i>
37
+ </div>
38
+
39
+ <pre id="code" data-sage--query-toggle-target="content"><code><%= @statement.try(:display_statement) || @query.statement %></code></pre>
40
+ </div>
34
41
 
35
42
  <% if @success %>
36
43
  <%= turbo_frame_tag dom_id(@query, 'results'), src: run_query_path(@query.id, from_show: true, variables: variable_params(@query)) do %>
data/config/importmap.rb CHANGED
@@ -5,6 +5,8 @@ pin "sage/controllers/clipboard_controller", to: "sage/controllers/clipboard_con
5
5
  pin "sage/controllers/select_controller", to: "sage/controllers/select_controller.js"
6
6
  pin "sage/controllers/dashboard_controller", to: "sage/controllers/dashboard_controller.js"
7
7
  pin "sage/controllers/reverse_infinite_scroll_controller", to: "sage/controllers/reverse_infinite_scroll_controller.js"
8
+ pin "sage/controllers/variables_controller", to: "sage/controllers/variables_controller.js"
9
+ pin "sage/controllers/query_toggle_controller", to: "sage/controllers/query_toggle_controller.js"
8
10
 
9
11
  # Don't pin common libraries - let the host app handle them
10
12
  # pin "@hotwired/stimulus", to: "stimulus.min.js"
data/lib/sage/engine.rb CHANGED
@@ -192,7 +192,8 @@ module Sage
192
192
  "sage/controllers/clipboard_controller.js",
193
193
  "sage/controllers/select_controller.js",
194
194
  "sage/controllers/dashboard_controller.js",
195
- "sage/controllers/reverse_infinite_scroll_controller.js"
195
+ "sage/controllers/reverse_infinite_scroll_controller.js",
196
+ "sage/controllers/query_toggle_controller.js"
196
197
  ]
197
198
 
198
199
  if defined?(Sprockets)
data/lib/sage/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Sage
2
- VERSION = "0.0.9"
2
+ VERSION = "0.1.0"
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.9
4
+ version: 0.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nathan Jones
@@ -152,6 +152,7 @@ files:
152
152
  - app/assets/javascripts/sage/application.js
153
153
  - app/assets/javascripts/sage/controllers/clipboard_controller.js
154
154
  - app/assets/javascripts/sage/controllers/dashboard_controller.js
155
+ - app/assets/javascripts/sage/controllers/query_toggle_controller.js
155
156
  - app/assets/javascripts/sage/controllers/reverse_infinite_scroll_controller.js
156
157
  - app/assets/javascripts/sage/controllers/search_controller.js
157
158
  - app/assets/javascripts/sage/controllers/select_controller.js
@@ -170,6 +171,7 @@ files:
170
171
  - app/javascript/sage.js
171
172
  - app/javascript/sage/controllers/clipboard_controller.js
172
173
  - app/javascript/sage/controllers/dashboard_controller.js
174
+ - app/javascript/sage/controllers/query_toggle_controller.js
173
175
  - app/javascript/sage/controllers/reverse_infinite_scroll_controller.js
174
176
  - app/javascript/sage/controllers/search_controller.js
175
177
  - app/javascript/sage/controllers/select_controller.js