Skip to content

Commit bfbb04c

Browse files
authored
Enchance/event invites (#1074)
This pull request introduces significant improvements to the event invitation workflow, debugging guidelines, and UI enhancements for person selection. The main changes include a new searchable person selector in the frontend, expanded controller logic for inviting existing users, improved debugging documentation, and more robust handling of invitation notifications and authorization. **Event Invitation Workflow Improvements** * Added an `available_people` endpoint to `Events::InvitationsController` to provide a filtered, searchable list of people eligible for invitation, excluding those already invited and those without email addresses. This endpoint supports search queries and returns results formatted for frontend consumption. * Updated invitation creation logic to support inviting existing users by ID, automatically populating email and locale from the selected person. * Improved invitation destruction and creation responses to support Turbo Streams, updating the UI dynamically and displaying flash messages. [[1]](diffhunk://#diff-f5bce0fdefe4021e576bd0afd9c7aa69a1fc93d255ac43b4677b6cd4a494d03eR38-R52) [[2]](diffhunk://#diff-f5bce0fdefe4021e576bd0afd9c7aa69a1fc93d255ac43b4677b6cd4a494d03eR134-R140) * Enhanced notification logic to distinguish between invitations for existing users (using in-app notifications) and invitations by email (using mailers). **Frontend Enhancements** * Added a new Stimulus controller `person_search_controller.js` that replaces a standard select box with a live-search input for person selection, calling the new backend endpoint and updating the select based on user choice. * Added new invitation-related styles in `invitations.scss` and imported them in the main stylesheet. [[1]](diffhunk://#diff-e0c8019729fad32b5ea701ddce7b7c208a2b77a40b6e4c9b082059a3c1169627R1-R5) [[2]](diffhunk://#diff-5499b5855ee11d3be02ebb1d25ee68dbe0331c7a2fc94250b73f716f42b803c6R34) **Debugging and Development Guidelines** * Updated documentation in `.github/copilot-instructions.md` and `AGENTS.md` to clarify that Rails console and runner should not be used for debugging. Instead, debugging should be performed through comprehensive tests and log analysis, with specific tips for using RSpec and temporary debug output. [[1]](diffhunk://#diff-227c2c26cb2ee0ce0f46a320fc48fbcbdf21801a57f59161b1d0861e8aad55f5R95-R107) [[2]](diffhunk://#diff-227c2c26cb2ee0ce0f46a320fc48fbcbdf21801a57f59161b1d0861e8aad55f5L105-R122) [[3]](diffhunk://#diff-a54ff182c7e8acf56acfd6e4b9c3ff41e2c41a31c9b211b2deb9df75d9a478f9R22-R32) [[4]](diffhunk://#diff-a54ff182c7e8acf56acfd6e4b9c3ff41e2c41a31c9b211b2deb9df75d9a478f9L32-R43) **Authorization and Notification Handling** * Improved authorization logic in `EventsController` and `ApplicationController`, ensuring proper handling for unauthenticated users and integrating invitation token authorization and notification reading. [[1]](diffhunk://#diff-ffc530a02a78834a7cc82d089dee80fa08f52d911015abfa0c563543b9fce194R6-R8) [[2]](diffhunk://#diff-ffc530a02a78834a7cc82d089dee80fa08f52d911015abfa0c563543b9fce194R31-R35) [[3]](diffhunk://#diff-353dd41f0d149f307449a2697955bbe8d70e73ea85c77b4a42ee9751273ba960R161-R169)
2 parents 7a0392b + bd5553d commit bfbb04c

File tree

176 files changed

+4505
-606
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

176 files changed

+4505
-606
lines changed

.github/copilot-instructions.md

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,19 @@ This repository contains the **Better Together Community Engine** (an isolated R
9292
9393
## Coding Guidelines
9494
95+
### Debugging and Development Practices
96+
- **Never use Rails console or runner for debugging** - These commands don't support our test-driven development approach
97+
- **Debug through comprehensive tests**: Write detailed test scenarios to reproduce, understand, and verify fixes for issues
98+
- **Use test-driven debugging workflow**:
99+
- Create specific tests that reproduce the problematic behavior
100+
- Add debugging assertions in tests to verify intermediate state
101+
- Trace through code by reading files and using grep search
102+
- Validate fixes by ensuring tests pass
103+
- **Leverage RSpec debugging tools**: Use `--format documentation` for detailed output, `fit` for focused testing, `puts` for temporary debug output in tests
104+
- **Analyze logs and error messages**: Examine Rails logs, test output, and stack traces for debugging information
105+
- **Read code systematically**: Use file reading tools to understand code paths and data flow
106+
- **Temporary debug output**: Add debug statements in application code if needed, but remove before committing
107+
95108
### Docker Environment Usage
96109
- **All database-dependent commands must use `bin/dc-run`**: This includes tests, generators, and any command that connects to PostgreSQL, Redis, or Elasticsearch
97110
- **Dummy app commands use `bin/dc-run-dummy`**: For Rails commands that need the dummy app context (console, migrations specific to dummy app)
@@ -102,10 +115,11 @@ This repository contains the **Better Together Community Engine** (an isolated R
102115
- RuboCop: `bin/dc-run bundle exec rubocop`
103116
- **IMPORTANT**: Never use `rspec -v` - this displays version info, not verbose output. Use `--format documentation` for detailed output.
104117
- **Examples of commands requiring `bin/dc-run-dummy`**:
105-
- Rails console: `bin/dc-run-dummy rails console`
118+
- Rails console: `bin/dc-run-dummy rails console` (for administrative tasks only, NOT for debugging)
106119
- Dummy app migrations: `bin/dc-run-dummy rails db:migrate`
107120
- Dummy app database operations: `bin/dc-run-dummy rails db:seed`
108121
- **Commands that don't require bin/dc-run**: File operations, documentation generation (unless database access needed), static analysis tools that don't connect to services
122+
- **CRITICAL**: Rails console and runner are NOT debugging tools in this project - use comprehensive test suites instead
109123
110124
### Security Requirements
111125
- **Run Brakeman before generating code**: `bin/dc-run bundle exec brakeman --quiet --no-pager`

.rubocop.yml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,21 @@ plugins:
1313
- rubocop-rspec_rails
1414
- rubocop-capybara
1515
- rubocop-factory_bot
16+
17+
# Disable all RSpec cops that affect test quality but not production code
18+
RSpec/ExampleLength:
19+
Enabled: false
1620
RSpec/MultipleExpectations:
1721
Enabled: false
22+
RSpec/MultipleMemoizedHelpers:
23+
Enabled: false
24+
RSpec/ContextWording:
25+
Enabled: false
26+
RSpec/MessageSpies:
27+
Enabled: false
28+
RSpec/LetSetup:
29+
Enabled: false
30+
1831
Style/StringLiterals:
1932
Exclude:
2033
- 'db/migrate/*'

AGENTS.md

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,17 @@ Instructions for GitHub Copilot and other automated contributors working in this
1919
- test: `community_engine_test`
2020
- Use `DATABASE_URL` to connect (overrides fallback host in `config/database.yml`).
2121

22+
## Debugging Guidelines
23+
- **Never use Rails console or runner for debugging** - These commands don't align with our test-driven development approach
24+
- **Use comprehensive test suites instead**: Write detailed tests to understand and verify system behavior
25+
- **Debug through tests**: Create specific test scenarios to reproduce and validate fixes for issues
26+
- **Use log analysis**: Examine Rails logs, test output, and error messages for debugging information
27+
- **Add temporary debugging assertions in tests**: Use `expect()` statements to verify intermediate state in tests
28+
- **Use RSpec debugging tools**: Use `--format documentation` for detailed test output, `fit` for focused testing
29+
- **Trace through code by reading files**: Use file reading and grep search to understand code paths
30+
- **Add debug output in application code temporarily** if needed, but remove before committing
31+
- **Validate fixes through test success**: Confirm that issues are resolved by having tests pass
32+
2233
## Commands
2334
- **Tests:** `bin/dc-run bin/ci`
2435
(Equivalent: `bin/dc-run bash -c "cd spec/dummy && bundle exec rspec"`)
@@ -29,7 +40,7 @@ Instructions for GitHub Copilot and other automated contributors working in this
2940
- Multiple specific lines: `bin/dc-run bundle exec rspec spec/file1_spec.rb:123 spec/file2_spec.rb:456`
3041
- **Important**: RSpec does NOT support hyphenated line numbers (e.g., `spec/file_spec.rb:123-456` is INVALID)
3142
- **Do NOT use `-v` flag**: The `-v` flag displays RSpec version information, NOT verbose output. Use `--format documentation` for detailed test descriptions.
32-
- **Rails Console:** `bin/dc-run-dummy rails console` (runs console in the dummy app context)
43+
- **Rails Console:** `bin/dc-run-dummy rails console` (for administrative tasks only - NOT for debugging. Use comprehensive tests for debugging instead)
3344
- **Rails Commands in Dummy App:** `bin/dc-run-dummy rails [command]` for any Rails commands that need the dummy app environment
3445
- **Lint:** `bin/dc-run bundle exec rubocop`
3546
- **Security:** `bin/dc-run bundle exec brakeman --quiet --no-pager` and `bin/dc-run bundle exec bundler-audit --update`
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
import { Controller } from "@hotwired/stimulus"
2+
3+
export default class extends Controller {
4+
static targets = ["select", "input"]
5+
static values = {
6+
searchUrl: String,
7+
searchDelay: { type: Number, default: 300 }
8+
}
9+
10+
connect() {
11+
this.setupPersonSearch()
12+
}
13+
14+
setupPersonSearch() {
15+
const select = this.selectTarget
16+
17+
// Convert select to a searchable input
18+
this.createSearchInput(select)
19+
20+
// Hide the original select
21+
select.style.display = 'none'
22+
}
23+
24+
createSearchInput(select) {
25+
const searchContainer = select.parentElement
26+
27+
// Create search input
28+
const searchInput = document.createElement('input')
29+
searchInput.type = 'text'
30+
searchInput.className = 'form-control person-search-input'
31+
searchInput.placeholder = select.options[0]?.text || 'Search for people...'
32+
searchInput.setAttribute('data-person-search-target', 'input')
33+
34+
// Create results dropdown
35+
const resultsDropdown = document.createElement('div')
36+
resultsDropdown.className = 'person-search-results'
37+
resultsDropdown.style.cssText = `
38+
position: absolute;
39+
top: 100%;
40+
left: 0;
41+
right: 0;
42+
background: white;
43+
border: 1px solid #ced4da;
44+
border-top: none;
45+
border-radius: 0 0 0.375rem 0.375rem;
46+
max-height: 200px;
47+
overflow-y: auto;
48+
z-index: 1000;
49+
display: none;
50+
`
51+
52+
// Insert elements
53+
searchContainer.style.position = 'relative'
54+
searchContainer.insertBefore(searchInput, select)
55+
searchContainer.appendChild(resultsDropdown)
56+
57+
// Setup event listeners
58+
let searchTimeout
59+
searchInput.addEventListener('input', (e) => {
60+
clearTimeout(searchTimeout)
61+
searchTimeout = setTimeout(() => {
62+
this.performSearch(e.target.value, resultsDropdown, select)
63+
}, this.searchDelayValue)
64+
})
65+
66+
searchInput.addEventListener('focus', () => {
67+
if (searchInput.value) {
68+
this.performSearch(searchInput.value, resultsDropdown, select)
69+
}
70+
})
71+
72+
// Hide dropdown when clicking outside
73+
document.addEventListener('click', (e) => {
74+
if (!searchContainer.contains(e.target)) {
75+
resultsDropdown.style.display = 'none'
76+
}
77+
})
78+
}
79+
80+
async performSearch(query, resultsDropdown, select) {
81+
if (query.length < 2) {
82+
resultsDropdown.style.display = 'none'
83+
return
84+
}
85+
86+
try {
87+
const response = await fetch(`${this.searchUrlValue}?search=${encodeURIComponent(query)}`, {
88+
headers: {
89+
'Accept': 'application/json',
90+
'X-Requested-With': 'XMLHttpRequest'
91+
}
92+
})
93+
94+
if (!response.ok) throw new Error('Search failed')
95+
96+
const people = await response.json()
97+
this.displayResults(people, resultsDropdown, select)
98+
} catch (error) {
99+
console.error('Person search error:', error)
100+
resultsDropdown.innerHTML = '<div class="p-2 text-danger">Search failed</div>'
101+
resultsDropdown.style.display = 'block'
102+
}
103+
}
104+
105+
displayResults(people, resultsDropdown, select) {
106+
if (people.length === 0) {
107+
resultsDropdown.innerHTML = '<div class="p-2 text-muted">No people found</div>'
108+
resultsDropdown.style.display = 'block'
109+
return
110+
}
111+
112+
const resultsHtml = people.map(person => `
113+
<div class="person-result p-2 border-bottom"
114+
style="cursor: pointer; display: flex; align-items: center;"
115+
data-person-id="${person.id}"
116+
data-person-name="${person.name}">
117+
<div class="me-2" style="width: 32px; height: 32px; background-color: #dee2e6; border-radius: 50%; display: flex; align-items: center; justify-content: center;">
118+
<i class="fas fa-user text-muted"></i>
119+
</div>
120+
<div>
121+
<div class="fw-medium">${person.name}</div>
122+
<small class="text-muted">@${person.slug}</small>
123+
</div>
124+
</div>
125+
`).join('')
126+
127+
resultsDropdown.innerHTML = resultsHtml
128+
resultsDropdown.style.display = 'block'
129+
130+
// Add click handlers to results
131+
resultsDropdown.querySelectorAll('.person-result').forEach(result => {
132+
result.addEventListener('click', () => {
133+
this.selectPerson(result, select)
134+
resultsDropdown.style.display = 'none'
135+
})
136+
})
137+
}
138+
139+
selectPerson(resultElement, select) {
140+
const personId = resultElement.dataset.personId
141+
const personName = resultElement.dataset.personName
142+
143+
// Update the hidden select
144+
select.innerHTML = `<option value="${personId}" selected>${personName}</option>`
145+
select.value = personId
146+
147+
// Update the search input
148+
const searchInput = this.inputTarget
149+
searchInput.value = personName
150+
151+
// Trigger change event for form handling
152+
select.dispatchEvent(new Event('change', { bubbles: true }))
153+
}
154+
}

app/assets/stylesheets/better_together/application.scss

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
@use 'conversations';
3232
@use 'forms';
3333
@use 'image-galleries';
34+
@use 'invitations';
3435
@use 'maps';
3536
@use 'metrics';
3637
@use 'navigation';
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
2+
.profile-image.invitee {
3+
width: 32px;
4+
height: 32px;
5+
}

app/controllers/better_together/application_controller.rb

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,13 @@ def user_not_authorized(exception) # rubocop:todo Metrics/AbcSize, Metrics/Metho
158158
]
159159
else
160160
flash[:error] = message # Use flash for regular redirects
161-
redirect_back(fallback_location: home_page_path)
161+
162+
# For unauthenticated users, redirect to login
163+
if current_user.nil?
164+
redirect_to new_user_session_path(locale: I18n.locale)
165+
else
166+
redirect_back(fallback_location: home_page_path)
167+
end
162168
end
163169
end
164170
# rubocop:enable Metrics/MethodLength

0 commit comments

Comments
 (0)