fix(frontend): a committee saves whatever the user list says - #1138
Merged
Conversation
CommitteeForm judged every member row against the page's `users` list, and that list is one page of an unbounded table fetched after the form is already open. A save was therefore refused, and no request left the browser, on what the page happened to know: #1042 fixed the empty list, #1099 the truncated one, and each time the sliver that was left kept the same failure shape — a submit that sends nothing, says nothing a request log can see, and leaves the board member looking at a form the api would have accepted. The rule is gone rather than narrowed again. Nothing on the api refuses a committee member who is not an association member: CommitteeService resolves the user by id, and CommitteeSeatRevocationListener gives the seat up when somebody stops being a member. A frontend-only guard reading data the page cannot hold whole, cannot hold fresh, and does not have yet is what produced three reports of the same bug. What is left is what the form can judge from itself: a row nobody has picked into is refused, which has to be said explicitly because 0 is what a fresh row carries, and a user twice in one committee is refused.
This was referenced Sep 5, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #1042.
Why
CommitteeFormjudged every member row against theuserslist the page holds, and that list is one page of an unbounded table fetched after the form is open. Whether a save left the browser therefore depended on what the page happened to know at the moment the button was pressed. That is the shape #1042 keeps coming back in: a submit that sends no request, shows no refusal a request log can see, and leaves a board member on a form the api would have accepted.Both earlier fixes narrowed the rule instead of removing it.
6c82c5b6cmade an empty list absent evidence;dfeffc8ebmade a member outside the page absent evidence and raised the page size to 500. Each left the rule holding a smaller sliver — after the second, the only refusal it can still produce is "the page's copy of this user says they are not a member" — and the failure shape survives in whatever sliver is left, because it is the reading of that list at all, not any particular reading, that decides whether the api is called.Nothing on the api refuses a committee member who is not an association member:
CommitteeService.reconcileMembersresolves the user by id and takes them, andCommitteeSeatRevocationListenergives the seat up when somebody stops being a member. So this was a frontend-only guard over a rule the api does not have, reading data the page cannot hold whole, cannot hold fresh, and does not have yet.What this achieves
A committee saves on what was typed into it. The user list is what fills the picker, and nothing else. A board member who edits a committee before
/usersanswers, or whose member sits outside the page it answered with, or whose copy of that user is stale, gets the save the api accepts.How
committeeUserIsMemberis gone. What replaces it,committeeMemberPicked, refuses only a row nobody has picked into — which has to be said explicitly, because0is what a fresh row carries andrequiredreads a number as filled in.uniqueCommitteeMemberstays: it judges the form against itself, not against a list. Both remaining refusals paint their message on the row they belong to.Not in scope
findUsers({size: 500})stays. With the rule gone it only decides what the picker can offer, and 500 offers more than the 20 it asked for before.Worth a reviewer's attention
creates committee from managerandupdates committee members and committee rolesfail deterministically — not on the save, but earlier, onCommitteeFormHelper.addMemberwaiting for a dropdown option for a user who is not in the first page. Say the word and I will file it; the fix is a picker that asks the api as you type rather than one that pretends to hold everything.Verification
Reproduction, at three levels:
saves an edited committee the user list contradictsinCommitteeManager.test.tsmounts the real page with real Vuetify, opens the edit form, types, and clicks the actual submit button. Againstmainit fails withexpected "vi.fn()" to be called 1 times, but got 0 times— the CI symptom exactly, a click that sends nothing. It passes here.GET /usersopen across the click, swept across fourteen arrival offsets from 0 to 80 ms, with the CPU throttled 20×, with reduced motion off. All green onmain— the empty and mid-flight cases are genuinely fixed, which is why the flake outlived those two fixes.GET /users?size=500fulfilled empty, aborted, and answered with a page the member is not on. All three saved. That is the issue's stated cause, dead in the real system.So the CI trace's
failed=[] requests=[… +12642ms [committee submit click]]is a refusal I could not provoke on any of those paths — and after this change there is no path left by which the user list can produce one. TheCommitteeManagerPageSystemTestclass runs green 3× against the real stack with this build.Local, each exit code checked:
yarn test:unit0,yarn lint0,yarn typecheck0. System tests: the one class, 3 runs,BUILD SUCCESSFUL; the rest of the suite was not run locally.Diff breakdown —
█added░removed, scaled to the largest row.