-
Notifications
You must be signed in to change notification settings - Fork 0
FIX: proper handling of group memberships #3
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: group-dm-user-addition-pre
Are you sure you want to change the base?
FIX: proper handling of group memberships #3
Conversation
|
@coderabbitai review |
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
WalkthroughGroup management system refactored to use pagination for member listing and dedicated REST endpoints. Member operations (add/remove) separated from group save flow. Admin group form template restructured with improved layout. Backend controllers updated with explicit create/update/destroy handlers. Changes
Sequence DiagramsequenceDiagram
participant User as Admin User
participant UI as Admin Group UI
participant Ctrl as Admin Controller
participant Model as Group Model
participant API as Backend API
rect rgb(200, 230, 255)
note over User,API: Group Member Pagination
User->>UI: View group members (page 1)
UI->>Ctrl: findMembers()
Ctrl->>Model: findMembers()
Model->>API: GET /groups/{name}/members.json?limit=50&offset=0
API-->>Model: members[], user_count, limit, offset
Model->>Model: Update pagination state
end
rect rgb(200, 230, 255)
note over User,API: Navigate to Next Page
User->>UI: Click next
UI->>Ctrl: next() action
Ctrl->>Model: findMembers() with updated offset
Model->>API: GET /groups/{name}/members.json?offset=50
API-->>Model: next batch of members
end
rect rgb(240, 200, 220)
note over User,API: Remove Member
User->>UI: Click remove on member
UI->>Ctrl: removeMember confirmation
Ctrl->>Model: removeMember(member)
Model->>API: DELETE /admin/groups/{id}/members.json
API-->>Model: success
Model->>Model: findMembers() to refresh
end
rect rgb(240, 200, 220)
note over User,API: Add Members
User->>UI: Submit usernames in form
UI->>Ctrl: addMembers() action
Ctrl->>Model: addMembers(usernames)
Model->>API: PUT /admin/groups/{id}/members.json
API-->>Model: success
Model->>Model: findMembers() to refresh
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 3
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
app/assets/javascripts/discourse/templates/group/members.hbs (1)
1-21: Fix stray</div>and align the loop/indentationTwo issues in this template section:
There’s a
</div>(Line 19) with no corresponding opening<div>, nested inside a<table>. This produces invalid markup and may cause browsers to restructure the DOM unexpectedly.The
{{#if}}still guards onmodelwhile the loop iteratesmembers. That’s harmless ifmodelis always truthy whenmembersis populated, but a more direct guard onmembers(ormembers.length) would be clearer.Suggested fix:
-{{#if model}} +{{#if members}} <table class='group-members'> @@ - {{#each m in members}} - <tr> + {{#each m in members}} + <tr> @@ - </tr> - </div> - {{/each}} + </tr> + {{/each}} </table> {{/if}}The key part is removing the stray
</div>; the{{#if members}}change is optional but improves readability.
🧹 Nitpick comments (13)
app/controllers/groups_controller.rb (1)
22-35: Consider clampinglimit/offsetto safe boundsThe pagination logic looks good and the meta payload is very useful. Two small robustness tweaks would help:
- Guard against pathological
limitvalues from params (e.g. very large numbers) to avoid heavy queries.- Ensure
offsetis never negative before callingoffset(offset)to avoid adapter/DB complaints on bad input.Example tweak:
- limit = (params[:limit] || 50).to_i - offset = params[:offset].to_i + limit = (params[:limit] || 50).to_i + limit = 50 if limit <= 0 + limit = [limit, 200].min + + offset = params[:offset].to_i + offset = 0 if offset < 0app/assets/stylesheets/common/admin/admin_base.scss (2)
164-182: Shared.form-horizontalstyles for.groups/.badgeslook reasonableCentralizing label weight, spacing, and control widths for
.groups/.badgesforms will keep those sections consistent and replaces more ad‑hoc rules elsewhere. Given.badges .form-horizontalhas an additional block later for badge‑specific tweaks, this layered approach is fine.If you start seeing subtle conflicts, consider extracting a reusable mixin for this layout instead of repeating
.form-horizontalrules in multiple places.
462-480: Groups area styles appear consistent, but rely heavily on!importantThe dedicated
.groupsrules for.ac-wrap, pagination links, and.btn.addalign with the new UI and should help override third‑party/component defaults.If possible, consider reducing the use of
!important(e.g. by tightening selector specificity) so these overrides remain easier to reason about in the long term.app/assets/javascripts/discourse/models/group.js (3)
20-37: PaginatedfindMembersimplementation looks soundThe guard on an empty
name, clampingoffsetto[0, user_count], and updatinguser_count/limit/offset/membersfrom the server response all look correct and match the new/groups/:name/members.jsonAPI.One small improvement would be to make it explicit that
membersis always an array ofDiscourse.Userinstances, e.g. via a brief comment or docstring, since callers may rely on that shape.
40-49: Consider returning thefindMembers()promise from thethencallbacks
removeMemberandaddMembersboth refresh the member list in a.then, but they don’t return thefindMembers()promise from inside the callback. That means callers only know about the completion of the add/remove request, not the subsequent reload.To allow clean chaining (
group.addMembers(...).then(...)after the list is fully refreshed), you can return thefindMembers()promise:- removeMember: function(member) { + removeMember: function(member) { var self = this; return Discourse.ajax('/admin/groups/' + this.get('id') + '/members.json', { type: "DELETE", data: { user_id: member.get("id") } - }).then(function() { - // reload member list - self.findMembers(); - }); + }).then(function() { + // reload member list + return self.findMembers(); + }); }, addMembers: function(usernames) { var self = this; return Discourse.ajax('/admin/groups/' + this.get('id') + '/members.json', { type: "PUT", data: { usernames: usernames } - }).then(function() { - // reload member list - self.findMembers(); - }) + }).then(function() { + // reload member list + return self.findMembers(); + }); },Also applies to: 51-60
62-84: InstanceasJSON/create/save/destroyAPIs fit the new REST endpointsThe flattened
asJSONpayload (name,alias_level,visible) and the RESTful instance methods for create/save/destroy align with the described backend changes.Two minor nits to consider:
create/destroyas instance method names can be slightly confusing alongside the class‑levelDiscourse.Group.create(...); a more explicit naming likecreateRemote/destroyRemotewould be clearer.- In
destroy, use an explicitreturn;to avoid style warnings:- destroy: function(){ - if (!this.get('id')) { return }; + destroy: function(){ + if (!this.get('id')) { return; } return Discourse.ajax("/admin/groups/" + this.get('id'), {type: "DELETE"}); },These are stylistic/clarity tweaks; behavior is otherwise fine.
app/assets/javascripts/admin/templates/group_member.hbs (1)
1-1: Confirmautomaticbinding source and consider accessible label on remove linkThe
{{#unless automatic}}guard assumesautomaticis available from the outer controller; if it isn’t, the remove link may show for automatic groups. Also, the icon-only remove anchor lacks any text/aria label, which is mildly problematic for screen readers; consider adding an accessible label ortitleattribute.app/assets/javascripts/admin/routes/admin_group_route.js (1)
11-14: Member loading insetupControlleris fine;set("model", model)is redundantCalling
model.findMembers()here is consistent with the rest of the flow. The explicitcontroller.set("model", model)merely repeats Ember’s defaultsetupControllerbehavior; you could drop it for brevity if there’s no special reason to keep it.spec/controllers/admin/groups_controller_spec.rb (2)
51-52: Avoid relying on hard‑coded automatic group id (1) in specsSeveral tests assume that
Group.find(1)/group_id: 1refers to an automatic group (Lines 51-52, 93-96, 115-116). That couples the specs to fixture/seed details and may become brittle if seeds change.Consider explicitly finding or fabricating an automatic group and using its id instead, for example:
- xhr :put, :update, id: 1, name: "WAT", visible: "true" + automatic_group = Group.find_by(automatic: true) + xhr :put, :update, id: automatic_group.id, name: "WAT", visible: "true"and similarly for the
add_members/remove_member422 tests.Also applies to: 93-96, 115-116
13-15: Clean up extra blank lines flagged by RuboCopRuboCop is reporting
Layout/EmptyLinesAroundBlockBodyfor a number of contexts and examples (e.g., extra blank lines immediately aftercontext/itdeclarations and beforeend). Trimming those will keep the spec file in line with the configured style and avoid noise in future diffs.Also applies to: 31-32, 35-36, 46-47, 50-51, 59-60, 77-78, 81-82, 88-89, 92-93, 109-110, 113-114, 131-132
app/controllers/admin/groups_controller.rb (2)
6-9: Clarify assignment inifconditions to satisfy RuboCop and readabilityLines 6-9 and 71-75 intentionally use assignment within an
if(if search = params[:search]andif user = User.find_by_username(username)), which is idiomatic Ruby but flagged byLint/AssignmentInCondition.To make intent explicit and silence the linter, you could wrap the assignment in parentheses or split it:
- if search = params[:search] - search = search.to_s + if (search = params[:search]) + search = search.to_s ... - usernames.split(",").each do |username| - if user = User.find_by_username(username) + usernames.split(",").each do |username| + if (user = User.find_by_username(username)) group.add(user) end endThis keeps behavior unchanged while improving clarity.
Also applies to: 71-75
84-97:remove_memberbehavior looks correct butgroup.savemay be unnecessary
remove_member(Lines 84-97) correctly:
- guards against automatic groups,
- removes the user via
group.users.delete(user_id), and- returns
success_json/render_json_error.Given Rails’ association semantics,
deleteon the association usually persists immediately, so the subsequentgroup.savemay be redundant. It’s harmless, though, and can be left as‑is unless you want to simplify.app/assets/javascripts/admin/controllers/admin-group.js.es6 (1)
51-65: Consider clearing the usernames input and handling async result inaddMembers
addMembers(Lines 61-65) sends the usernames string to the model but leaves the input populated (// TODO: should clear the input) and doesn’t react to success/failure ofmodel.addMembers(...).You could improve UX by clearing the field on success and optionally surfacing errors, e.g.:
- addMembers: function() { - // TODO: should clear the input - if (Em.isEmpty(this.get("usernames"))) { return; } - this.get("model").addMembers(this.get("usernames")); - }, + addMembers: function() { + var usernames = this.get("usernames"); + if (Em.isEmpty(usernames)) { return; } + + var self = this; + this.get("model").addMembers(usernames).then(function() { + self.set("usernames", ""); + }); + },Exact promise handling can be adjusted to match the
Groupmodel API.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (16)
app/assets/javascripts/admin/controllers/admin-group.js.es6(2 hunks)app/assets/javascripts/admin/routes/admin_group_route.js(1 hunks)app/assets/javascripts/admin/templates/group.hbs(1 hunks)app/assets/javascripts/admin/templates/group_member.hbs(1 hunks)app/assets/javascripts/admin/views/group-member.js.es6(1 hunks)app/assets/javascripts/discourse/models/group.js(1 hunks)app/assets/javascripts/discourse/routes/group-members.js.es6(1 hunks)app/assets/javascripts/discourse/templates/components/admin-group-selector.hbs(0 hunks)app/assets/javascripts/discourse/templates/group/members.hbs(1 hunks)app/assets/javascripts/discourse/templates/user-selector-autocomplete.raw.hbs(1 hunks)app/assets/stylesheets/common/admin/admin_base.scss(2 hunks)app/controllers/admin/groups_controller.rb(2 hunks)app/controllers/groups_controller.rb(1 hunks)config/locales/client.en.yml(1 hunks)config/routes.rb(1 hunks)spec/controllers/admin/groups_controller_spec.rb(2 hunks)
💤 Files with no reviewable changes (1)
- app/assets/javascripts/discourse/templates/components/admin-group-selector.hbs
🧰 Additional context used
🪛 RuboCop (1.81.7)
spec/controllers/admin/groups_controller_spec.rb
[convention] 14-15: Extra empty line detected at block body beginning.
(Layout/EmptyLinesAroundBlockBody)
[convention] 31-32: Extra empty line detected at block body end.
(Layout/EmptyLinesAroundBlockBody)
[convention] 35-36: Extra empty line detected at block body beginning.
(Layout/EmptyLinesAroundBlockBody)
[convention] 46-47: Extra empty line detected at block body end.
(Layout/EmptyLinesAroundBlockBody)
[convention] 50-51: Extra empty line detected at block body beginning.
(Layout/EmptyLinesAroundBlockBody)
[convention] 59-60: Extra empty line detected at block body end.
(Layout/EmptyLinesAroundBlockBody)
[convention] 77-78: Extra empty line detected at block body end.
(Layout/EmptyLinesAroundBlockBody)
[convention] 81-82: Extra empty line detected at block body beginning.
(Layout/EmptyLinesAroundBlockBody)
[convention] 88-89: Extra empty line detected at block body end.
(Layout/EmptyLinesAroundBlockBody)
[convention] 92-93: Extra empty line detected at block body beginning.
(Layout/EmptyLinesAroundBlockBody)
[convention] 109-110: Extra empty line detected at block body end.
(Layout/EmptyLinesAroundBlockBody)
[convention] 113-114: Extra empty line detected at block body beginning.
(Layout/EmptyLinesAroundBlockBody)
[convention] 131-132: Extra empty line detected at block body end.
(Layout/EmptyLinesAroundBlockBody)
app/controllers/admin/groups_controller.rb
[warning] 6-6: Use == if you meant to do a comparison or wrap the expression in parentheses to indicate you meant to assign in a condition.
(Lint/AssignmentInCondition)
[warning] 72-72: Use == if you meant to do a comparison or wrap the expression in parentheses to indicate you meant to assign in a condition.
(Lint/AssignmentInCondition)
🔇 Additional comments (11)
app/assets/javascripts/discourse/templates/user-selector-autocomplete.raw.hbs (1)
5-8: Formatting-only changes look fineIndentation/structure adjustments around the
<a>contents don’t alter any helpers or control flow. Markup remains valid and behavior should be unchanged.Also applies to: 15-19
app/controllers/groups_controller.rb (1)
40-45:find_groupreformatting is fineLogic in
find_groupis unchanged; the method still enforces visibility viaguardian.ensure_can_see!. No issues here.app/assets/javascripts/discourse/models/group.js (1)
10-12: User count + display helper are straightforwardDefaulting
limit,offset, anduser_counton the model and hiding a zerouserCountDisplayis clean and matches the new paginated members flow. No issues here.Also applies to: 17-17
app/assets/javascripts/admin/views/group-member.js.es6 (1)
1-4: View wiring for group member item looks correctThe view cleanly wires the CSS class and template name, no issues spotted.
config/locales/client.en.yml (1)
1616-1634: New/updated admin group translation keys look consistentThe updated
selector_placeholderand the new member-management strings (delete_member_confirm,name,add,add_members) are coherent and match the admin group UI. Just ensure the confirm dialog is invoked with bothusernameandgroupvariables populated.app/assets/javascripts/discourse/routes/group-members.js.es6 (1)
8-12: Route now cleanly reuses group model and triggers member loadUsing
modelFor('group')and then callingmodel.findMembers()insetupControlleris a straightforward pattern and keeps thegroupandgroup-membersroutes in sync. Just ensurefindMembers()is safe to call multiple times (e.g., on route re-entry).config/routes.rb (1)
45-51: Admin member-management routes are well-scopedThe new
DELETE /admin/groups/membersandPUT /admin/groups/membersroutes are correctly nested underresources :groupswithAdminConstraint, so they’re admin-only and align with the new add/remove member actions. Ensure the controller consistently expects group/user identifiers via params or body for both routes.app/assets/javascripts/admin/templates/group.hbs (1)
12-32: Member listing and add-members flow look correct; verify pager and counts stay in syncThe guarded member list/add-members sections (
{{#if id}},{{#unless automatic}}) nicely enforce “no member edits for unsaved or automatic groups.” Pagination controls anduser_countare exposed cleanly to the template. Please just double‑check that:
showingFirst/showingLastare honored in theprevious/nextactions (so clicking when disabled is a no‑op), anduser_count,members, and pagination state are updated consistently after add/remove operations.spec/controllers/admin/groups_controller_spec.rb (1)
13-132: Expanded controller specs look consistent with new behaviorsThe added/structured contexts for
.index,.create,.update,.destroy,.refresh_automatic_groups,.add_members, and.remove_memberaccurately exercise the happy paths and the automatic‑group guard rails. JSON expectations on.index, trimming behavior on.create, and the member add/remove scenarios all line up with the controller logic without obvious gaps.Please run the controller specs for this file to confirm everything passes against the current schema and serializers.
app/controllers/admin/groups_controller.rb (1)
3-16: Controller CRUD and automatic‑group guards are consistent with the new API
index,create,update,destroy,refresh_automatic_groups, andcan_not_modify_automaticare wired coherently:
- explicit attribute assignment avoids mass‑assignment issues,
- automatic groups are protected from destructive changes, and
- JSON responses (
render_serializedvssuccess_json/render_json_error) align with the specs.No functional issues stand out in these paths.
Please confirm via the existing controller specs that create/update/destroy/refresh flows behave as expected for both automatic and regular groups.
Also applies to: 22-47, 49-63, 99-104
app/assets/javascripts/admin/controllers/admin-group.js.es6 (1)
51-59: Member removal and save flows are consistent with the server APIThe
removeMemberaction’s confirmation flow and call tomodel.removeMember(member)(Lines 51-59) match the new backend endpoints, and the updatedsaveanddestroyactions (Lines 67-90, 92-110) correctly delegate togroup.save(),group.create(), andgroup.destroy()while managingdisableSave.These look sound as long as the corresponding model methods return promises as expected.
Consider quickly smoke‑testing member removal and group save/delete in the admin UI to ensure the promises and notifications behave as intended.
Also applies to: 67-90, 92-110
| currentPage: function() { | ||
| if (this.get("user_count") == 0) { return 0; } | ||
| return Math.floor(this.get("offset") / this.get("limit")) + 1; | ||
| }.property("limit", "offset", "user_count"), | ||
|
|
||
| totalPages: function() { | ||
| if (this.get("user_count") == 0) { return 0; } | ||
| return Math.floor(this.get("user_count") / this.get("limit")) + 1; | ||
| }.property("limit", "user_count"), | ||
|
|
||
| showingFirst: Em.computed.lte("currentPage", 1), | ||
| showingLast: Discourse.computed.propertyEqual("currentPage", "totalPages"), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
Fix off‑by‑one error in totalPages pagination logic
totalPages (Lines 11-15) computes:
if (this.get("user_count") == 0) { return 0; }
return Math.floor(this.get("user_count") / this.get("limit")) + 1;This overcounts when user_count is an exact multiple of limit (e.g., 20 users, page size 10 → returns 3 pages). Combined with the next action (Lines 29-38), this allows navigation to an extra, empty page and an unnecessary findMembers() call.
Use Math.ceil instead:
- totalPages: function() {
- if (this.get("user_count") == 0) { return 0; }
- return Math.floor(this.get("user_count") / this.get("limit")) + 1;
- }.property("limit", "user_count"),
+ totalPages: function() {
+ if (this.get("user_count") == 0) { return 0; }
+ return Math.ceil(this.get("user_count") / this.get("limit"));
+ }.property("limit", "user_count"),This keeps showingFirst/showingLast and next/previous in sync with the actual number of pages.
Please verify pagination in the UI with user counts that are and are not multiples of the page size (e.g., 10, 11, 20 members).
Also applies to: 29-50
🏁 Script executed:
#!/bin/bash
# Find and examine the admin-group controller file
find . -name "*admin-group*" -type f 2>/dev/null | head -20Length of output: 275
🏁 Script executed:
#!/bin/bash
# Extract the relevant sections from the admin-group controller
cat ./app/assets/javascripts/admin/controllers/admin-group.js.es6 2>/dev/null | head -60Length of output: 1928
🏁 Script executed:
#!/bin/bash
# Search for pagination-related tests or validation
find . -name "*test*" -o -name "*spec*" | xargs grep -l "totalPages\|currentPage" 2>/dev/null | head -10Length of output: 0
🏁 Script executed:
#!/bin/bash
# Check if there are any guards or clamping logic in the next/previous actions
rg -n "next|previous" ./app/assets/javascripts/admin/controllers/admin-group.js.es6 -A 5 -B 1 2>/dev/nullLength of output: 451
🏁 Script executed:
#!/bin/bash
# Examine the group model's findMembers method to understand side effects
rg -n "findMembers" ./app/assets/javascripts/models/ -A 3 2>/dev/null | head -30Length of output: 0
🏁 Script executed:
#!/bin/bash
# Check if showingLast property is used to prevent navigation beyond last page
rg -n "showingLast" ./app/assets/javascripts/admin/ -B 2 -A 2 2>/dev/null | head -30Length of output: 1528
Fix off‑by‑one error in totalPages pagination logic
totalPages (lines 11-14) computes:
if (this.get("user_count") == 0) { return 0; }
return Math.floor(this.get("user_count") / this.get("limit")) + 1;This overcounts when user_count is an exact multiple of limit (e.g., 20 users, page size 10 → returns 3 instead of 2). The showingLast guard at line 30 fails to prevent navigation to the extra page because both currentPage and totalPages are overcounted, so the comparison never blocks at the actual boundary. This allows navigation to empty pages and unnecessary findMembers() calls.
Use Math.ceil instead:
- totalPages: function() {
- if (this.get("user_count") == 0) { return 0; }
- return Math.floor(this.get("user_count") / this.get("limit")) + 1;
- }.property("limit", "user_count"),
+ totalPages: function() {
+ if (this.get("user_count") == 0) { return 0; }
+ return Math.ceil(this.get("user_count") / this.get("limit"));
+ }.property("limit", "user_count"),This keeps pagination boundaries consistent with showingFirst, showingLast, and the next/previous actions.
🤖 Prompt for AI Agents
In app/assets/javascripts/admin/controllers/admin-group.js.es6 around lines 6 to
17, totalPages currently uses Math.floor(user_count / limit) + 1 which
overcounts when user_count is an exact multiple of limit; replace that
computation with Math.ceil(user_count / limit) (keeping the zero-check) so
totalPages correctly reflects the number of pages and aligns with
showingFirst/showingLast and next/previous navigation.
| <form class="form-horizontal"> | ||
|
|
||
| <div class="control-group"> | ||
| <label class="control-label">{{i18n 'admin.groups.group_members'}}</label> | ||
| <div class="controls"> | ||
| {{user-selector usernames=usernames id="group-users" placeholderKey="admin.groups.selector_placeholder" tabindex="1" disabled=automatic}} | ||
| <div> | ||
| {{#if automatic}} | ||
| <h3>{{name}}</h3> | ||
| {{else}} | ||
| <label for="name">{{i18n 'admin.groups.name'}}</label> | ||
| {{text-field name="name" value=name placeholderKey="admin.groups.name_placeholder"}} | ||
| {{/if}} | ||
| </div> | ||
| </div> | ||
| <div class="control-group"> | ||
| <div class="controls"> | ||
| {{input type="checkbox" checked=visible}} {{i18n 'groups.visible'}} | ||
|
|
||
| {{#if id}} | ||
| <div> | ||
| <label>{{i18n 'admin.groups.group_members'}} ({{user_count}})</label> | ||
| <div> | ||
| <a {{bind-attr class=":previous showingFirst:disabled"}} {{action "previous"}}>{{fa-icon "fast-backward"}}</a> | ||
| {{currentPage}}/{{totalPages}} | ||
| <a {{bind-attr class=":next showingLast:disabled"}} {{action "next"}}>{{fa-icon "fast-forward"}}</a> | ||
| </div> | ||
| <div class="ac-wrap clearfix"> | ||
| {{each member in members itemView="group-member"}} | ||
| </div> | ||
| </div> | ||
|
|
||
| {{#unless automatic}} | ||
| <div> | ||
| <label for="user-selector">{{i18n 'admin.groups.add_members'}}</label> | ||
| {{user-selector usernames=usernames placeholderKey="admin.groups.selector_placeholder" id="user-selector"}} | ||
| <button {{action "addMembers"}} class='btn add'>{{fa-icon "plus"}} {{i18n 'admin.groups.add'}}</button> | ||
| </div> | ||
| {{/unless}} | ||
| {{/if}} | ||
|
|
||
| <div> | ||
| <label> | ||
| {{input type="checkbox" checked=visible}} | ||
| {{i18n 'groups.visible'}} | ||
| </label> | ||
| </div> | ||
| </div> | ||
| <div class="control-group"> | ||
| <label class="control-label">{{i18n 'groups.alias_levels.title'}}</label> | ||
| <div class="controls"> | ||
| {{combo-box valueAttribute="value" value=alias_level content=aliasLevelOptions}} | ||
|
|
||
| <div> | ||
| <label for="alias">{{i18n 'groups.alias_levels.title'}}</label> | ||
| {{combo-box name="alias" valueAttribute="value" value=alias_level content=aliasLevelOptions}} | ||
| </div> | ||
| </div> | ||
| <div class='controls'> | ||
| <button {{action "save"}} {{bind-attr disabled="disableSave"}} class='btn'>{{i18n 'admin.customize.save'}}</button> | ||
| {{#unless automatic}} | ||
| <button {{action "destroy"}} class='btn btn-danger'><i class="fa fa-trash-o"></i>{{i18n 'admin.customize.delete'}}</button> | ||
| {{/unless}} | ||
| </div> | ||
|
|
||
| <div class='buttons'> | ||
| <button {{action "save"}} {{bind-attr disabled="disableSave"}} class='btn btn-primary'>{{i18n 'admin.customize.save'}}</button> | ||
| {{#unless automatic}} | ||
| <button {{action "destroy"}} class='btn btn-danger'>{{fa-icon "trash-o"}}{{i18n 'admin.customize.delete'}}</button> | ||
| {{/unless}} | ||
| </div> | ||
|
|
||
| </form> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Handle form submit explicitly to avoid native browser submission
Wrapping everything in <form class="form-horizontal"> but only wiring actions on buttons means pressing Enter in a field may trigger a native form submit (page reload) instead of the intended Ember save action. Consider adding an explicit submit handler on the form, e.g. {{action "save" on="submit"}}, and preventing default there so keyboard submit paths behave the same as clicking Save.
🤖 Prompt for AI Agents
In app/assets/javascripts/admin/templates/group.hbs around lines 1-53 the form
currently relies on button actions only, so pressing Enter can trigger a native
browser submit; add an explicit submit handler to the form (e.g. attach {{action
"save" on="submit"}} to the <form>) and update the corresponding "save" action
in the component/controller to accept the submit event and call
event.preventDefault() (or return false) before performing the existing save
logic so keyboard/Enter submits invoke the Ember save flow instead of a page
reload.
| def add_members | ||
| group = Group.find(params.require(:group_id).to_i) | ||
| usernames = params.require(:usernames) | ||
|
|
||
| return can_not_modify_automatic if group.automatic | ||
|
|
||
| usernames.split(",").each do |username| | ||
| if user = User.find_by_username(username) | ||
| group.add(user) | ||
| end | ||
| end | ||
|
|
||
| if group.save | ||
| render_serialized(group, BasicGroupSerializer) | ||
| render json: success_json | ||
| else | ||
| render_json_error group | ||
| render_json_error(group) | ||
| end | ||
| end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
Trim and validate usernames when adding members
In add_members (Lines 65-82), usernames.split(",") is used directly. If the client sends "bob, alice", the second entry becomes " alice" and User.find_by_username will fail to resolve it. Blank segments (e.g. trailing comma) would also be processed.
Consider normalizing and filtering before lookup:
- usernames.split(",").each do |username|
- if user = User.find_by_username(username)
+ usernames.split(",").map(&:strip).reject(&:blank?).each do |username|
+ if (user = User.find_by_username(username))
group.add(user)
end
endThis makes the endpoint robust to common input formats from the UI.
Please verify this behavior in the admin UI by trying inputs with spaces and trailing commas.
I'll verify the review comment by examining the User model implementation and how usernames are handled in the codebase.
---
🏁 Script executed:
```shell
#!/bin/bash
# Find User model definition and find_by_username method
fd -t f "user\.rb|User\.rb" | head -20
Length of output: 1055
🏁 Script executed:
#!/bin/bash
# Search for find_by_username implementation
rg -n "find_by_username" --type=ruby -C5Length of output: 13311
🏁 Script executed:
#!/bin/bash
# Look for username field definition in models
rg -n "username" --type=ruby -C2 | grep -E "(t\.|attribute|validates|column)" | head -30Length of output: 3890
🏁 Script executed:
#!/bin/bash
# Check for other username processing patterns in the codebase
rg -n "split.*," --type=ruby -C3 | grep -E "(username|user)" | head -30Length of output: 2465
---
🏁 Script executed:
```shell
#!/bin/bash
# Verify if there are tests for add_members with whitespace
rg -n "add_members" --type=ruby -C10 | head -50
Length of output: 3278
Trim and validate usernames when adding members to prevent silent failures
In add_members (Lines 65-82), usernames.split(",") is used directly without normalizing whitespace. If the client sends "bob, alice", the second entry becomes " alice" with a leading space. Since User.find_by_username calls only .downcase (not .strip), it attempts find_by(username_lower: " alice"), which fails silently—the user is not added to the group.
Apply this fix to normalize and filter input:
- usernames.split(",").each do |username|
- if user = User.find_by_username(username)
+ usernames.split(",").map(&:strip).reject(&:blank?).each do |username|
+ if (user = User.find_by_username(username))
group.add(user)
end
endThis handles trailing commas and spaces commonly sent from UI input fields.
🧰 Tools
🪛 RuboCop (1.81.7)
[warning] 72-72: Use == if you meant to do a comparison or wrap the expression in parentheses to indicate you meant to assign in a condition.
(Lint/AssignmentInCondition)
🤖 Prompt for AI Agents
In app/controllers/admin/groups_controller.rb around lines 65 to 82 the
usernames list is split by commas but not normalized, so entries like " alice"
(leading/trailing spaces) or empty strings from trailing commas cause
User.find_by_username to miss users silently; update the handler to split on
commas, strip whitespace from each entry, reject blank entries (and optionally
uniq them) before looking up users and adding them to the group so only valid,
normalized usernames are used.
Test 8
Summary by CodeRabbit
New Features
Bug Fixes
UI/UX Improvements
✏️ Tip: You can customize this high-level summary in your review settings.
Replicated from ai-code-review-evaluation/discourse-coderabbit#8