Skip to content

Commit 2fe5ce3

Browse files
committed
Enhance conversation and user management flows
- Updated the messaging flow diagram to include error handling for adding participants. - Improved the events location selector flow diagram with clearer method labels. - Added new diagrams for platform manager functionalities: user administration, invitation system, user support, and security monitoring. - Created user authentication flow diagram to illustrate the sign-in process. - Refactored user management flow diagram to serve as an index linking to detailed sub-diagrams. - Introduced user profile management flow diagram to outline profile editing processes. - Implemented client-side validation for conversation creation to prevent empty messages. - Enhanced conversation factory to ensure initial messages are present for validation. - Added tests for conversation creation with initial messages and profile message prefill functionality. - Updated resource toolbar to support additional actions via block content.
1 parent e0af8d7 commit 2fe5ce3

File tree

86 files changed

+903
-358
lines changed

Some content is hidden

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

86 files changed

+903
-358
lines changed

.github/copilot-instructions.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ This repository contains the **Better Together Community Engine** (an isolated R
116116
- **Use allow-lists for dynamic class resolution**: Follow the `joatu_source_class` pattern with concern-based allow-lists
117117
- **Validate user inputs**: Always sanitize and validate parameters, especially for file uploads and dynamic queries
118118
- **Strong parameters**: Use Rails strong parameters in all controllers
119+
- **Model-level permitted attributes**: Prefer defining a class method `self.permitted_attributes` on models that returns the permitted attribute array (including nested attributes). Controllers and shared resource code should call `Model.permitted_attributes` rather than hard-coding permit lists. Compose nested permitted attributes by referencing other models' `permitted_attributes` (for example: `Conversation.permitted_attributes` may include `{ messages_attributes: Message.permitted_attributes }`).
119120
- **Authorization everywhere**: Implement Pundit policy checks on all actions
120121
- **SQL injection prevention**: Use parameterized queries, avoid string interpolation in SQL
121122
- **XSS prevention**: Use Rails auto-escaping, sanitize HTML inputs with allowlists
@@ -205,6 +206,35 @@ This repository contains the **Better Together Community Engine** (an isolated R
205206
- **Rails-Controller-Testing**: Add `gem 'rails-controller-testing'` to Gemfile for `assigns` method in controller tests.
206207
- Toggle requires_invitation and provide invitation_code when needed for registration tests.
207208

209+
### Automatic test configuration & auth helper patterns
210+
211+
This repository provides an automatic test-configuration layer (see `spec/support/automatic_test_configuration.rb`) that sets up the host `Platform` and, where appropriate, performs authentication for request, controller, and feature specs so most specs do NOT need to call `configure_host_platform` manually.
212+
213+
- Automatic setup applies to specs with `type: :request`, `type: :controller`, and `type: :feature` by default.
214+
- Use these example metadata tags to control authentication explicitly:
215+
- `:as_platform_manager` or `:platform_manager` — login as the platform manager (elevated privileges)
216+
- `:as_user`, `:authenticated`, or `:user` — login as a regular user
217+
- `:no_auth` or `:unauthenticated`ensure no authentication is performed for the example
218+
- `:skip_host_setup` — skip host platform creation/configuration for this example
219+
220+
How it works:
221+
- The test helper inspects example metadata and description text (describe/context). If the description contains keywords such as "platform manager", "admin", "authenticated", or "signed in", it will automatically set appropriate tags and perform the corresponding authentication.
222+
- The helper creates a host `Platform` if one does not exist and marks the default setup wizard as completed.
223+
- For request specs it uses HTTP login helpers (`login(email, password)`); for controller specs it uses Devise test helpers (`sign_in`); for feature specs it uses Capybara UI login flows.
224+
225+
Recommended usage:
226+
- Prefer using metadata tags (`:as_platform_manager`, `:as_user`, `:skip_host_setup`) in the `describe` or `context` header when a test needs a specific authentication state. Example:
227+
228+
```ruby
229+
RSpec.describe 'Creating a conversation', type: :request, :as_user do
230+
# host platform and user login are automatically configured
231+
end
232+
```
233+
234+
- Avoid calling `configure_host_platform` manually in most specs; reserve manual calls for special cases (use `:skip_host_setup` to opt out of automatic config).
235+
236+
Note: The helper set lives under `spec/support/automatic_test_configuration.rb` and provides helpers like `configure_host_platform`, `find_or_create_test_user`, and `capybara_login_as_platform_manager` to use directly if needed by unusual tests.
237+
208238
### Testing Architecture Standards
209239
- **Project Standard**: Use request specs (`type: :request`) for all controller testing to maintain consistency
210240
- **Request Specs Advantages**: Handle Rails engine routing automatically through full HTTP stack

AGENTS.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ Instructions for GitHub Copilot and other automated contributors working in this
5050
- Use allow-lists for dynamic class resolution (see `joatu_source_class` pattern)
5151
- Sanitize and validate all user inputs
5252
- Use strong parameters in controllers
53+
- Define model-level permitted attributes: prefer a class method `self.permitted_attributes` on models that returns the permitted attribute list (including nested attribute structures). Controllers should call `Model.permitted_attributes` to build permit lists instead of hard-coding them. When composing nested attributes, reference other models' `permitted_attributes` (for example: `Conversation.permitted_attributes` may include `{ messages_attributes: Message.permitted_attributes }`).
5354
- Implement proper authorization checks (Pundit policies)
5455
- **For reflection-based features**: Create concerns with `included_in_models` class methods for safe dynamic class resolution
5556
- **Post-generation security check**: Run `bin/dc-run bundle exec brakeman --quiet --no-pager -c UnsafeReflection,SQL,CrossSiteScripting` after major code changes
@@ -226,6 +227,35 @@ For every implementation plan, create acceptance criteria covering relevant stak
226227
- **Required for**: Controller specs, request specs, feature specs, and any integration tests that involve routing or authentication.
227228
- **Locale Parameters**: Engine controller tests require locale parameters (e.g., `params: { locale: I18n.default_locale }`) due to routing constraints.
228229

230+
### Automatic test configuration & auth helper patterns
231+
232+
This repository provides an automatic test-configuration layer (see `spec/support/automatic_test_configuration.rb`) that sets up the host `Platform` and, where appropriate, performs authentication for request, controller, and feature specs so most specs do NOT need to call `configure_host_platform` manually.
233+
234+
- Automatic setup applies to specs with `type: :request`, `type: :controller`, and `type: :feature` by default.
235+
- Use these example metadata tags to control authentication explicitly:
236+
- `:as_platform_manager` or `:platform_manager` — login as the platform manager (elevated privileges)
237+
- `:as_user`, `:authenticated`, or `:user` — login as a regular user
238+
- `:no_auth` or `:unauthenticated` — ensure no authentication is performed for the example
239+
- `:skip_host_setup` — skip host platform creation/configuration for this example
240+
241+
How it works:
242+
- The test helper inspects example metadata and description text (describe/context). If the description contains keywords such as "platform manager", "admin", "authenticated", or "signed in", it will automatically set appropriate tags and perform the corresponding authentication.
243+
- The helper creates a host `Platform` if one does not exist and marks the default setup wizard as completed.
244+
- For request specs it uses HTTP login helpers (`login(email, password)`); for controller specs it uses Devise test helpers (`sign_in`); for feature specs it uses Capybara UI login flows.
245+
246+
Recommended usage:
247+
- Prefer using metadata tags (`:as_platform_manager`, `:as_user`, `:skip_host_setup`) in the `describe` or `context` header when a test needs a specific authentication state. Example:
248+
249+
```ruby
250+
RSpec.describe 'Creating a conversation', type: :request, :as_user do
251+
# host platform and user login are automatically configured
252+
end
253+
```
254+
255+
- Avoid calling `configure_host_platform` manually in most specs; reserve manual calls for special cases (use `:skip_host_setup` to opt out of automatic config).
256+
257+
Note: The helper set lives under `spec/support/automatic_test_configuration.rb` and provides helpers like `configure_host_platform`, `find_or_create_test_user`, and `capybara_login_as_platform_manager` to use directly if needed by unusual tests.
258+
229259
## Test Coverage Standards
230260
- **Models**: Test validations, associations, scopes, instance methods, class methods, and callbacks.
231261
- **Controllers**: Test all actions, authorization policies, parameter handling, and response formats.
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// Styles specific to the shared resource toolbar
2+
3+
.resource-toolbar {
4+
display: flex;
5+
align-items: center;
6+
}
7+
8+
.resource-toolbar-extra {
9+
display: flex;
10+
align-items: center;
11+
margin-left: auto; // Fallback in case .ms-auto is unavailable
12+
gap: 0.5rem; // Consistent spacing for appended actions
13+
}
14+

app/assets/stylesheets/better_together/application.scss

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
@use 'share';
3838
@use 'sidebar_nav';
3939
@use 'trix-extensions/richtext';
40+
@use 'resource_toolbar';
4041

4142
// Styles that use the variables
4243
.text-opposite-theme {
@@ -104,4 +105,3 @@
104105
.spin-horizontal {
105106
animation: flip 1s linear infinite;
106107
}
107-

app/assets/stylesheets/better_together/conversations.scss

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,3 +93,74 @@
9393
overflow-y: auto;
9494
}
9595

96+
/* Responsive tweaks for conversation header & participants */
97+
/* Reduce header padding and font size on small screens */
98+
@media (max-width: 991.98px) { // Bootstrap lg breakpoint
99+
/* Reduce overall header vertical padding on small screens */
100+
.card-header {
101+
padding-top: 0.25rem !important;
102+
padding-bottom: 0.25rem !important;
103+
}
104+
105+
/* Make the title smaller when present */
106+
.card-header h4 {
107+
font-size: 1rem; /* smaller header */
108+
margin: 0;
109+
}
110+
111+
/* Allow header children to wrap so we can place participants on their own row */
112+
.card-header {
113+
flex-wrap: wrap;
114+
}
115+
116+
/* Reduce padding on the back link (left arrow) to save vertical space */
117+
.card-header > a.p-4 {
118+
padding-top: 0.25rem !important;
119+
padding-bottom: 0.25rem !important;
120+
padding-left: 0.5rem !important;
121+
padding-right: 0.5rem !important;
122+
}
123+
124+
/* Reduce horizontal margin around the options dropdown icon */
125+
.card-header .mx-4 {
126+
margin-left: 0.5rem !important;
127+
margin-right: 0.5rem !important;
128+
}
129+
130+
/* Place the participants container on its own full-width row below the header
131+
when an h4 title exists, but keep participant items inline (horizontal). */
132+
.card-header h4 + .conversation-participants {
133+
flex: 0 0 100% !important;
134+
order: 3;
135+
width: 100%;
136+
display: flex !important;
137+
flex-direction: row !important; /* keep items side-by-side */
138+
align-items: center !important;
139+
justify-content: space-evenly !important; /* distribute participants evenly */
140+
gap: 0.5rem;
141+
margin-top: 0.5rem;
142+
overflow-x: auto;
143+
}
144+
145+
/* Small spacing adjustments for individual participant blocks when moved below */
146+
.card-header h4 + .conversation-participants .mention_person {
147+
margin-right: 0.5rem !important;
148+
padding-top: 0.15rem;
149+
padding-bottom: 0.15rem;
150+
}
151+
152+
/* Shrink avatar size for participants in the header row to reduce height */
153+
.card-header h4 + .conversation-participants .mention_person .profile-image {
154+
width: 32px !important;
155+
height: 32px !important;
156+
object-fit: cover;
157+
}
158+
159+
/* Ensure the options (ellipsis) container hugs the right edge of the header */
160+
.card-header > .align-self-center.mx-4 {
161+
margin-left: auto !important;
162+
order: 2; /* keep it on the top row before participants (participants are order:3) */
163+
align-self: center !important;
164+
}
165+
}
166+

app/controllers/better_together/conversations_controller.rb

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,23 @@ class ConversationsController < ApplicationController # rubocop:todo Metrics/Cla
1616
helper_method :available_participants
1717

1818
def index
19-
authorize @conversations
19+
# Conversations list is prepared by set_conversations (before_action)
20+
# Provide a blank conversation for the new-conversation form in the sidebar
21+
@conversation = Conversation.new
22+
authorize @conversation
2023
end
2124

2225
def new
23-
@conversation = Conversation.new
26+
if params[:conversation].present?
27+
conv_params = params.require(:conversation).permit(:title, participant_ids: [])
28+
@conversation = Conversation.new(conv_params)
29+
else
30+
@conversation = Conversation.new
31+
end
32+
33+
# Ensure nested message is available for the form (so users can create the first message inline)
34+
@conversation.messages.build if @conversation.messages.empty?
35+
2436
authorize @conversation
2537
end
2638

@@ -32,6 +44,13 @@ def create # rubocop:todo Metrics/MethodLength, Metrics/AbcSize
3244

3345
@conversation = Conversation.new(filtered_params.merge(creator: helpers.current_person))
3446

47+
# If nested messages were provided, ensure the sender is set to the creator/current person
48+
if @conversation.messages.any?
49+
@conversation.messages.each do |m|
50+
m.sender = helpers.current_person
51+
end
52+
end
53+
3554
authorize @conversation
3655

3756
if submitted_any && filtered_empty
@@ -52,7 +71,7 @@ def create # rubocop:todo Metrics/MethodLength, Metrics/AbcSize
5271
end
5372
end
5473
elsif @conversation.save
55-
@conversation.participants << helpers.current_person
74+
@conversation.add_participant_safe(helpers.current_person)
5675

5776
respond_to do |format|
5877
format.turbo_stream
@@ -203,7 +222,8 @@ def available_participants
203222
end
204223

205224
def conversation_params
206-
params.require(:conversation).permit(:title, participant_ids: [])
225+
# Use model-defined permitted attributes so nested attributes composition stays DRY
226+
params.require(:conversation).permit(*Conversation.permitted_attributes)
207227
end
208228

209229
# Ensure participant_ids only include people the agent is allowed to message.

app/controllers/better_together/messages_controller.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ def set_conversation
2828
end
2929

3030
def message_params
31-
params.require(:message).permit(:content)
31+
params.require(:message).permit(*BetterTogether::Message.permitted_attributes)
3232
end
3333

3434
def notify_participants(message)

app/controllers/better_together/navigation_areas_controller.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ def set_navigation_area
101101
end
102102

103103
def navigation_area_params
104-
params.require(:navigation_area).permit(:name, :slug, :visible, :style, :protected)
104+
params.require(:navigation_area).permit(*resource_class.permitted_attributes)
105105
end
106106

107107
def resource_class

app/controllers/better_together/navigation_items_controller.rb

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -137,11 +137,7 @@ def navigation_item
137137
end
138138

139139
def navigation_item_params
140-
params.require(:navigation_item).permit(
141-
:navigation_area_id, :url, :icon, :position, :visible,
142-
:item_type, :linkable_id, :parent_id, :route_name,
143-
*resource_class.localized_attribute_list
144-
)
140+
params.require(:navigation_item).permit(*resource_class.permitted_attributes)
145141
end
146142

147143
def resource_class

app/helpers/better_together/form_helper.rb

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -116,32 +116,48 @@ def privacy_field(form:, klass:)
116116
end
117117

118118
# rubocop:todo Metrics/MethodLength
119-
def required_label(form_or_object, field, **options) # rubocop:todo Metrics/AbcSize, Metrics/MethodLength
119+
# Accepts an optional label_text override which, when provided, will be used
120+
# instead of the model's human_attribute_name for the field. This is useful
121+
# when the visible label needs to be different from the translated attribute
122+
# name (for example: participant_ids -> "Add participants").
123+
# rubocop:todo Metrics/PerceivedComplexity
124+
def required_label(form_or_object, field, label_text: nil, **options) # rubocop:todo Metrics/AbcSize, Metrics/MethodLength
120125
# Determine if it's a form object or just an object
121126
if form_or_object.respond_to?(:object)
122127
object = form_or_object.object
123-
label_text = object.class.human_attribute_name(field)
128+
# Use provided label_text override if present, otherwise fall back to translation
129+
label_text ||= object.class.human_attribute_name(field)
124130
class_name = options.delete(:class_name)
125131

126132
# Use the provided class_name for validation check if present, otherwise use the object's class
127133
else
128134
object = form_or_object
129-
label_text = object.class.human_attribute_name(field)
135+
label_text ||= object.class.human_attribute_name(field)
130136

131137
# Use the provided class_name for validation check if present, otherwise use the object's class
132138
end
139+
133140
klass = class_name ? class_name.constantize : object.class
134141
is_required = class_field_required(klass, field)
135142

136-
# Append asterisk for required fields
137-
label_text += " <span class='required-indicator'>*</span>" if is_required
143+
# Append asterisk for required fields and attach tooltip to the asterisk
144+
if is_required
145+
tooltip_text = I18n.t('helpers.required_info', default: 'This field is required')
146+
# Make the asterisk keyboard-focusable and allow the tooltip to be
147+
# triggered by click as well as hover/focus so it works on mobile.
148+
asterisk = content_tag(:span, '*', class: 'required-indicator', tabindex: 0, role: 'button',
149+
data: { bs_toggle: 'tooltip', bs_trigger: 'hover focus click' },
150+
title: tooltip_text)
151+
label_text += " #{asterisk}"
152+
end
138153

139154
if form_or_object.respond_to?(:label)
140155
form_or_object.label(field, label_text.html_safe, options)
141156
else
142157
label_tag(field, label_text.html_safe, options)
143158
end
144159
end
160+
# rubocop:enable Metrics/PerceivedComplexity
145161
# rubocop:enable Metrics/MethodLength
146162

147163
# rubocop:todo Metrics/MethodLength

0 commit comments

Comments
 (0)