lsa_tdx_feedback 1.0.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.
data/README.md ADDED
@@ -0,0 +1,391 @@
1
+ # LsaTdxFeedback
2
+
3
+ A self-contained Rails gem for collecting user feedback via TeamDynamix (TDX) API for LSA applications. LsaTdxFeedback provides a secure, configurable solution that integrates seamlessly with any Rails application.
4
+
5
+ ## Features
6
+
7
+ - **Secure Configuration**: All values must be provided by your application for maximum security
8
+ - **Self-Contained UI**: Beautiful, responsive feedback modal with its own CSS and JavaScript
9
+ - **TDX Integration**: Direct integration with TeamDynamix API for ticket creation
10
+ - **OAuth Authentication**: Secure client credentials flow authentication with tdxticket scope
11
+ - **Mobile Responsive**: Works perfectly on desktop and mobile devices
12
+ - **Framework Agnostic**: CSS and JavaScript work independently of your app's styling
13
+ - **Automatic Context**: Captures page URL, user agent, and user email automatically
14
+ - **Caching**: Smart token caching for optimal performance
15
+
16
+ ## Installation
17
+
18
+ Add this line to your application's Gemfile:
19
+
20
+ ```ruby
21
+ gem 'lsa_tdx_feedback'
22
+ ```
23
+
24
+ And then execute:
25
+
26
+ ```bash
27
+ $ bundle install
28
+ ```
29
+
30
+ ## Configuration
31
+
32
+ ### Rails Credentials (Required)
33
+
34
+ **Security Note**: All configuration values must be provided by your application. No default values are stored in the gem for security reasons.
35
+
36
+ Add your TDX API credentials to your Rails credentials:
37
+
38
+ ```bash
39
+ $ rails credentials:edit
40
+ ```
41
+
42
+ ```yaml
43
+ lsa_tdx_feedback:
44
+ # Required OAuth Configuration
45
+ oauth_url: 'https://your-tdx-instance.com/oauth2' # Note: Do NOT include /token
46
+ api_base_url: 'https://your-tdx-instance.com/api'
47
+ client_id: your_tdx_client_id
48
+ client_secret: your_tdx_client_secret
49
+
50
+ # Required TDX Configuration
51
+ app_id: 123
52
+ account_id: 456
53
+ service_offering_id: 789
54
+
55
+ # Required Ticket Configuration
56
+ default_type_id: 100
57
+ default_form_id: 200
58
+ default_classification: '300'
59
+ default_status_id: 400
60
+ default_priority_id: 500
61
+ default_source_id: 600
62
+ default_responsible_group_id: 700
63
+ default_service_id: 800
64
+ ```
65
+
66
+ ### Manual Configuration
67
+
68
+ If you prefer not to use Rails credentials, you can configure the gem manually in an initializer:
69
+
70
+ ```ruby
71
+ # config/initializers/lsa_tdx_feedback.rb
72
+ LsaTdxFeedback.configure do |config|
73
+ # Required OAuth Configuration
74
+ config.oauth_url = ENV['TDX_OAUTH_URL'] # Note: Do NOT include /token
75
+ config.api_base_url = ENV['TDX_API_BASE_URL']
76
+ config.client_id = ENV['TDX_CLIENT_ID']
77
+ config.client_secret = ENV['TDX_CLIENT_SECRET']
78
+
79
+ # Required TDX Configuration
80
+ config.app_id = ENV['TDX_APP_ID'].to_i
81
+ config.account_id = ENV['TDX_ACCOUNT_ID'].to_i
82
+ config.service_offering_id = ENV['TDX_SERVICE_OFFERING_ID'].to_i
83
+
84
+ # Required Ticket Configuration
85
+ config.default_type_id = ENV['TDX_TYPE_ID'].to_i
86
+ config.default_form_id = ENV['TDX_FORM_ID'].to_i
87
+ config.default_classification = ENV['TDX_CLASSIFICATION']
88
+ config.default_status_id = ENV['TDX_STATUS_ID'].to_i
89
+ config.default_priority_id = ENV['TDX_PRIORITY_ID'].to_i
90
+ config.default_source_id = ENV['TDX_SOURCE_ID'].to_i
91
+ config.default_responsible_group_id = ENV['TDX_RESPONSIBLE_GROUP_ID'].to_i
92
+ config.default_service_id = ENV['TDX_SERVICE_ID'].to_i
93
+ end
94
+ ```
95
+
96
+ **Note**: If you use Rails credentials (recommended), the gem will automatically configure itself and you don't need to create this initializer file.
97
+
98
+ ## Usage
99
+
100
+ ### Step 1: Mount the Engine Routes
101
+
102
+ Add the feedback gem routes to your application's `config/routes.rb`:
103
+
104
+ ```ruby
105
+ # config/routes.rb
106
+ Rails.application.routes.draw do
107
+ # Your existing routes...
108
+
109
+ # Mount the feedback gem engine
110
+ mount LsaTdxFeedback::Engine => "/lsa_tdx_feedback", as: "lsa_tdx_feedback"
111
+ end
112
+ ```
113
+
114
+ ### Step 2: Add the Feedback Modal and Assets (optimal placement)
115
+
116
+ For best performance and predictable ordering, include CSS in `<head>`, render the modal markup in the body, and place JS just before `</body>`:
117
+
118
+ ```erb
119
+ <!-- app/views/layouts/application.html.erb -->
120
+
121
+ <head>
122
+ <%= lsa_tdx_feedback_css %>
123
+ <!-- your other tags ... -->
124
+ ...
125
+ ...
126
+ <%= csrf_meta_tags %>
127
+ <%= csp_meta_tag %>
128
+ <!-- etc. -->
129
+ ...
130
+ ...
131
+ ...
132
+ ...
133
+ </head>
134
+
135
+ <body>
136
+ ...
137
+ <%= lsa_tdx_feedback_modal %>
138
+ <%= lsa_tdx_feedback_js %>
139
+ </body>
140
+ ```
141
+
142
+ This avoids FOUC (flash of unstyled content) and ensures the script loads after the DOM.
143
+
144
+ ### Advanced Usage
145
+
146
+ #### Performance vs convenience
147
+
148
+ - **Optimal loading (recommended)**: Use the separate helpers so CSS loads in `<head>` and JS loads just before `</body>`. This avoids FOUC and keeps asset order predictable.
149
+ - **Convenience**: Use `lsa_tdx_feedback` (all‑in‑one) for quick setup. It injects CSS/JS plus the modal where it’s called, which can delay CSS and is less ideal for performance.
150
+
151
+ #### Recommended placement (optimal)
152
+
153
+ ```erb
154
+ <!-- app/views/layouts/application.html.erb -->
155
+
156
+ <head>
157
+ <%= lsa_tdx_feedback_css %>
158
+ <!-- your other tags ... -->
159
+ ...
160
+ ...
161
+ </head>
162
+
163
+ <body>
164
+ ...
165
+ <%= lsa_tdx_feedback_modal %>
166
+ <%= lsa_tdx_feedback_js %>
167
+ </body>
168
+ ```
169
+
170
+ You can also include components separately for more control over placement:
171
+
172
+ ```erb
173
+ <!-- In your layout head section -->
174
+ <%= lsa_tdx_feedback_css %>
175
+
176
+ <!-- In your layout body section (before closing </body> tag) -->
177
+ <%= lsa_tdx_feedback_js %>
178
+
179
+ <!-- Include just the modal (assets must be included separately) -->
180
+ <%= lsa_tdx_feedback_modal %>
181
+ ```
182
+
183
+ Or use the combined assets helper:
184
+
185
+ ```erb
186
+ <!-- Include both CSS and JavaScript together -->
187
+ <%= lsa_tdx_feedback_assets %>
188
+
189
+ <!-- Include just the modal (assets must be included separately) -->
190
+ <%= lsa_tdx_feedback_modal %>
191
+ ```
192
+
193
+ #### When to use explicit Rails asset tags
194
+
195
+ If you prefer explicit control, you can use Rails helpers directly:
196
+
197
+ ```erb
198
+ <head>
199
+ <%= stylesheet_link_tag 'lsa_tdx_feedback', media: 'all', 'data-turbo-track': 'reload' %>
200
+ </head>
201
+
202
+ <body>
203
+ ...
204
+ <%= lsa_tdx_feedback_modal %>
205
+ <%= javascript_include_tag 'lsa_tdx_feedback', defer: true, 'data-turbo-track': 'reload' %>
206
+ </body>
207
+ ```
208
+
209
+ Use explicit tags when you need to:
210
+
211
+ - Set attributes like `defer`, `async`, `nonce` (CSP), `integrity`, `crossorigin`, `media`, or `preload`
212
+ - Control exact ordering relative to Bootstrap or your packs
213
+ - Swap delivery (CDN vs pipeline) or pin versions independently of the gem
214
+ - Integrate precisely with your asset setup (Importmap, Propshaft, Sprockets, etc.)
215
+
216
+ #### Convenience (all‑in‑one)
217
+
218
+ If you want a single helper that injects CSS, JS, and the modal where it’s called (good for quick trials or prototypes):
219
+
220
+ ```erb
221
+ <!-- In your application layout (e.g., app/views/layouts/application.html.erb) -->
222
+ <%= lsa_tdx_feedback %>
223
+ ```
224
+
225
+ Note: This may load CSS later than ideal and is less optimal for performance.
226
+
227
+ ### Customizing User Email
228
+
229
+ By default, LsaTdxFeedback tries to automatically detect the current user's email. You can customize this by overriding the `current_user_email_for_feedback` method in your ApplicationController:
230
+
231
+ ```ruby
232
+ class ApplicationController < ActionController::Base
233
+ private
234
+
235
+ def current_user_email_for_feedback
236
+ current_user&.email_address # or however you access user email
237
+ end
238
+ end
239
+ ```
240
+
241
+ ## TDX API Configuration
242
+
243
+ ### Required TDX Settings
244
+
245
+ 1. **Client Credentials**: You need OAuth client credentials from your TDX administrator
246
+ 2. **Responsible Group ID**: The TDX group that will receive feedback tickets
247
+ 3. **App ID**: Your TDX ticketing application ID (usually provided by your TDX admin)
248
+
249
+ ### Default TDX Values
250
+
251
+ The gem uses these default values based on University of Michigan's TDX setup:
252
+
253
+ - **OAuth URL**: *see API documentation*
254
+ - **API Base URL**: *see API documentation*
255
+ - **Type ID**: 28 (TeamDynamix)
256
+ - **Form ID**: 20 (Request Form)
257
+ - **Classification**: "46" (Request)
258
+ - **Status ID**: 77 (New)
259
+ - **Priority ID**: 20 (Medium)
260
+ - **Source ID**: 8 (Systems)
261
+ - **Service ID**: 67 (ITS-TeamDynamix Support)
262
+
263
+ You can override any of these in your configuration.
264
+
265
+ ## Feedback Categories
266
+
267
+ The modal includes predefined categories that automatically set ticket priorities:
268
+
269
+ - **Bug Report** → High Priority
270
+ - **Urgent/Critical** → Emergency Priority
271
+ - **Suggestion/Enhancement/Feature** → Low Priority
272
+ - **General/Other** → Medium Priority (default)
273
+
274
+ ## Styling
275
+
276
+ The gem includes completely self-contained CSS that won't interfere with your application's styles. All CSS classes are prefixed with `feedback-gem-` to avoid conflicts.
277
+
278
+ If you need to customize the appearance, you can override the styles in your application:
279
+
280
+ ```css
281
+ /* Customize the trigger button */
282
+ .feedback-gem-trigger-btn {
283
+ background: #custom-color !important;
284
+ }
285
+
286
+ /* Customize the modal */
287
+ .feedback-gem-modal-content {
288
+ border-radius: 8px !important;
289
+ }
290
+ ```
291
+
292
+ ## JavaScript
293
+
294
+ The gem includes self-contained JavaScript that initializes automatically. It handles:
295
+
296
+ - Modal show/hide functionality
297
+ - Form submission via AJAX
298
+ - Loading states and error handling
299
+ - Keyboard navigation (ESC to close)
300
+ - Mobile responsive behavior
301
+
302
+ ## Development
303
+
304
+ After checking out the repo, run:
305
+
306
+ ```bash
307
+ $ bundle install
308
+ $ bundle exec rake spec # Run tests
309
+ $ bundle exec rubocop # Run linter
310
+ ```
311
+
312
+ ## API Reference
313
+
314
+ ### Configuration Options
315
+
316
+ ```ruby
317
+ LsaTdxFeedback.configure do |config|
318
+ # Required
319
+ config.client_id = "your_client_id"
320
+ config.client_secret = "your_client_secret"
321
+ config.default_responsible_group_id = 123
322
+
323
+ # Optional TDX settings
324
+ config.oauth_url = "https://your-tdx-oauth-url"
325
+ config.api_base_url = "https://your-tdx-api-url"
326
+ config.app_id = 31
327
+ config.default_service_id = 67
328
+ config.default_type_id = 28
329
+ config.default_form_id = 20
330
+ config.default_classification = "46"
331
+ config.default_status_id = 77
332
+ config.default_priority_id = 20
333
+ config.default_source_id = 8
334
+
335
+ # Caching
336
+ config.cache_store = :redis_cache_store
337
+ config.cache_expiry = 3600 # Token cache expiry in seconds
338
+ end
339
+ ```
340
+
341
+ ### View Helpers
342
+
343
+ - `lsa_tdx_feedback` - Includes both assets and modal (all-in-one)
344
+ - `lsa_tdx_feedback_assets` - Includes both CSS and JavaScript
345
+ - `lsa_tdx_feedback_css` - Includes only the CSS stylesheet
346
+ - `lsa_tdx_feedback_js` - Includes only the JavaScript file
347
+ - `lsa_tdx_feedback_modal` - Includes only the modal HTML
348
+
349
+ ## Contributing
350
+
351
+ Bug reports and pull requests are welcome on GitHub at https://github.com/lsa-mis/lsa_feedback.
352
+
353
+ ## License
354
+
355
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
356
+
357
+ ## Troubleshooting
358
+
359
+ ### Common Issues
360
+
361
+ 1. **"client_id is required" error**
362
+ - Make sure you've configured your TDX credentials properly
363
+ - Check that your Rails credentials are accessible
364
+
365
+ 2. **Modal doesn't appear or form submission fails**
366
+ - Ensure you've included `<%= lsa_tdx_feedback %>` in your layout
367
+ - **Check that you've mounted the engine routes** in your `config/routes.rb`
368
+ - Check browser console for JavaScript errors
369
+ - Verify the `/lsa_tdx_feedback/feedback` endpoint is accessible
370
+ - If using separate helpers, ensure CSS is in `<head>` and JS is before `</body>`
371
+
372
+ 3. **Styling conflicts**
373
+ - All LsaTdxFeedback styles are prefixed with `feedback-gem-`
374
+ - Use browser dev tools to check for CSS conflicts
375
+
376
+ 4. **OAuth/API errors**
377
+ - Verify your TDX credentials are correct
378
+ - Check that your TDX instance URLs are correct
379
+ - **Ensure oauth_url does NOT include /token** (e.g., use `https://your-tdx.com/oauth2` not `https://your-tdx.com/oauth2/token`)
380
+ - Ensure your responsible group ID exists in TDX
381
+
382
+ ### Debug Mode
383
+
384
+ Enable debug logging by setting the Rails log level:
385
+
386
+ ```ruby
387
+ # In development.rb or production.rb
388
+ config.log_level = :debug
389
+ ```
390
+
391
+ This will log OAuth token requests and API calls for troubleshooting.
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rspec/core/rake_task'
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ require 'rubocop/rake_task'
7
+
8
+ RuboCop::RakeTask.new
9
+
10
+ task default: %i[spec rubocop]
@@ -0,0 +1,231 @@
1
+ /**
2
+ * LsaTdxFeedback JavaScript - Self-contained feedback modal functionality
3
+ */
4
+ (function() {
5
+ 'use strict';
6
+
7
+ // Ensure we don't initialize multiple times
8
+ if (window.LsaTdxFeedback && window.LsaTdxFeedback.initialized) {
9
+ return;
10
+ }
11
+
12
+ var LsaTdxFeedback = {
13
+ initialized: false,
14
+ modal: null,
15
+ form: null,
16
+
17
+ init: function() {
18
+ if (this.initialized) return;
19
+
20
+ // Wait for DOM to be ready
21
+ if (document.readyState === 'loading') {
22
+ document.addEventListener('DOMContentLoaded', this.init.bind(this));
23
+ return;
24
+ }
25
+
26
+ this.bindEvents();
27
+ this.initialized = true;
28
+ },
29
+
30
+ bindEvents: function() {
31
+ var self = this;
32
+
33
+ // Get modal and form elements
34
+ this.modal = document.getElementById('lsa-tdx-feedback-modal');
35
+ this.form = document.getElementById('lsa-tdx-feedback-form');
36
+
37
+ if (!this.modal || !this.form) {
38
+ console.warn('LsaTdxFeedback: Modal or form not found. Make sure to include the feedback modal partial.');
39
+ return;
40
+ }
41
+
42
+ // Trigger button
43
+ var triggerBtn = document.getElementById('lsa-tdx-feedback-trigger');
44
+ if (triggerBtn) {
45
+ triggerBtn.addEventListener('click', function(e) {
46
+ e.preventDefault();
47
+ self.showModal();
48
+ });
49
+ }
50
+
51
+ // Close buttons
52
+ var closeBtn = this.modal.querySelector('.lsa-tdx-feedback-close-btn');
53
+ var cancelBtn = this.modal.querySelector('.lsa-tdx-feedback-cancel-btn');
54
+ var backdrop = this.modal.querySelector('.lsa-tdx-feedback-modal-backdrop');
55
+
56
+ if (closeBtn) {
57
+ closeBtn.addEventListener('click', function(e) {
58
+ e.preventDefault();
59
+ self.hideModal();
60
+ });
61
+ }
62
+
63
+ if (cancelBtn) {
64
+ cancelBtn.addEventListener('click', function(e) {
65
+ e.preventDefault();
66
+ self.hideModal();
67
+ });
68
+ }
69
+
70
+ if (backdrop) {
71
+ backdrop.addEventListener('click', function(e) {
72
+ if (e.target === backdrop) {
73
+ self.hideModal();
74
+ }
75
+ });
76
+ }
77
+
78
+ // Form submission
79
+ this.form.addEventListener('submit', function(e) {
80
+ e.preventDefault();
81
+ self.submitFeedback();
82
+ });
83
+
84
+ // Escape key to close modal
85
+ document.addEventListener('keydown', function(e) {
86
+ if (e.key === 'Escape' && self.modal.style.display !== 'none') {
87
+ self.hideModal();
88
+ }
89
+ });
90
+ },
91
+
92
+ showModal: function() {
93
+ if (!this.modal) return;
94
+
95
+ this.modal.style.display = 'block';
96
+ document.body.style.overflow = 'hidden';
97
+
98
+ // Focus on first input
99
+ var firstInput = this.modal.querySelector('select, input, textarea');
100
+ if (firstInput) {
101
+ setTimeout(function() {
102
+ firstInput.focus();
103
+ }, 100);
104
+ }
105
+
106
+ this.hideMessage();
107
+ },
108
+
109
+ hideModal: function() {
110
+ if (!this.modal) return;
111
+
112
+ this.modal.style.display = 'none';
113
+ document.body.style.overflow = '';
114
+ this.resetForm();
115
+ this.hideMessage();
116
+ },
117
+
118
+ resetForm: function() {
119
+ if (!this.form) return;
120
+
121
+ this.form.reset();
122
+ this.setSubmitButtonState(false);
123
+ },
124
+
125
+ submitFeedback: function() {
126
+ var self = this;
127
+ var formData = new FormData(this.form);
128
+
129
+ // Basic validation
130
+ var feedback = formData.get('feedback');
131
+ if (!feedback || feedback.trim().length === 0) {
132
+ this.showMessage('Please enter your feedback.', 'error');
133
+ return;
134
+ }
135
+
136
+ this.setSubmitButtonState(true);
137
+ this.hideMessage();
138
+
139
+ // Get CSRF token
140
+ var csrfToken = this.getCSRFToken();
141
+
142
+ // Prepare data
143
+ var data = {
144
+ feedback: {
145
+ category: formData.get('category'),
146
+ feedback: feedback,
147
+ email: formData.get('email'),
148
+ url: formData.get('url'),
149
+ user_agent: formData.get('user_agent'),
150
+ additional_info: formData.get('additional_info')
151
+ }
152
+ };
153
+
154
+ // Submit via fetch
155
+ fetch('/lsa_tdx_feedback/feedback', {
156
+ method: 'POST',
157
+ headers: {
158
+ 'Content-Type': 'application/json',
159
+ 'X-CSRF-Token': csrfToken,
160
+ 'X-Requested-With': 'XMLHttpRequest'
161
+ },
162
+ body: JSON.stringify(data)
163
+ })
164
+ .then(function(response) {
165
+ return response.json().then(function(data) {
166
+ return { response: response, data: data };
167
+ });
168
+ })
169
+ .then(function(result) {
170
+ self.setSubmitButtonState(false);
171
+
172
+ if (result.response.ok && result.data.success) {
173
+ self.showMessage(result.data.message || 'Thank you for your feedback!', 'success');
174
+ setTimeout(function() {
175
+ self.hideModal();
176
+ }, 2000);
177
+ } else {
178
+ self.showMessage(result.data.message || 'There was an error submitting your feedback.', 'error');
179
+ }
180
+ })
181
+ .catch(function(error) {
182
+ console.error('LsaTdxFeedback submission error:', error);
183
+ self.setSubmitButtonState(false);
184
+ self.showMessage('There was an error submitting your feedback. Please try again.', 'error');
185
+ });
186
+ },
187
+
188
+ setSubmitButtonState: function(loading) {
189
+ var submitBtn = this.modal.querySelector('.lsa-tdx-feedback-submit-btn');
190
+ var btnText = submitBtn.querySelector('.lsa-tdx-feedback-btn-text');
191
+ var spinner = submitBtn.querySelector('.lsa-tdx-feedback-loading-spinner');
192
+
193
+ if (loading) {
194
+ submitBtn.disabled = true;
195
+ btnText.style.display = 'none';
196
+ spinner.style.display = 'inline-flex';
197
+ } else {
198
+ submitBtn.disabled = false;
199
+ btnText.style.display = 'inline';
200
+ spinner.style.display = 'none';
201
+ }
202
+ },
203
+
204
+ showMessage: function(message, type) {
205
+ var messageEl = document.getElementById('lsa-tdx-feedback-message');
206
+ var contentEl = messageEl.querySelector('.lsa-tdx-feedback-message-content');
207
+
208
+ contentEl.textContent = message;
209
+ messageEl.className = 'lsa-tdx-feedback-message ' + (type || 'info');
210
+ messageEl.style.display = 'block';
211
+ },
212
+
213
+ hideMessage: function() {
214
+ var messageEl = document.getElementById('lsa-tdx-feedback-message');
215
+ if (messageEl) {
216
+ messageEl.style.display = 'none';
217
+ }
218
+ },
219
+
220
+ getCSRFToken: function() {
221
+ var token = document.querySelector('meta[name="csrf-token"]');
222
+ return token ? token.getAttribute('content') : '';
223
+ }
224
+ };
225
+
226
+ // Export to global scope
227
+ window.LsaTdxFeedback = LsaTdxFeedback;
228
+
229
+ // Auto-initialize
230
+ LsaTdxFeedback.init();
231
+ })();