Repository navigation
Add public input to the conference committee page - #2303
Conversation
Testimony on a committee of conference answers a different question than testimony on a bill: which version the committee should report, not whether the bill should pass. Position gains passNothing, passSomething, passSenateVersion and passHouseVersion. A draft with a conferenceId must take one of them; every other draft keeps the three bill positions. It follows ballot questions: one Testimony type scoped by an optional conferenceId, with counts kept on the conference document. The testimony is filed against the conference's canonical bill but kept off it: not in its counts or latest testimony, not in testimony search, and not sent to the bill's followers. The author's followers still see it. A user can now hold a bill testimony and a conference testimony on one bill, so publish, delete, drafts and the version archive match the scope in memory instead of by query. That avoids stamping conferenceId: null on existing documents. Excluding conference testimony from search changes the indexed collection's filter, so each environment rebuilds the testimony collection on its next deploy. One composite index is added for the conference listing. Rolling back once conference testimony exists leaves older functions unable to publish or delete it. This is the server side only. Nothing under components/ or pages/ changes, so there is no way to file conference testimony from the site yet. Verified: functions and site tsc; functions unit tests for testimony and notifications (32); the testimony, ballot question, moderation, conferences and count backfill integration suites (59 tests), run on the branch tip where the functions are unchanged from this commit. Not verified: that testimony search leaves conference testimony out (no Typesense locally); publishing on dev. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
This should go in after #2299 |
Conference testimony is filed against the conference's canonical bill, so it shares that bill's id with the bill's own testimony. The site now keeps the two apart the way the server does. The client gets its own ConferencePosition, ConferenceTestimony and conference draft types, so the bill and ballot-question displays keep their three positions and none of them change. Listings for a bill, a profile and recent testimony skip documents with a conferenceId. Draft and publication lookups match the conference scope with the functions' matchesTestimonyScope, so a conference draft cannot collide with the bill's draft. useEditConferenceTestimony drafts and publishes on a conference, and useConferenceTestimony lists what has been filed on one. Verified: site tsc is clean. The scope match is the functions' matchesTestimonyScope, whose unit test passes. On the emulator a conference testimony was drafted, posted and republished through the real publish function, and its draft carried the conferenceId. Not verified: a bill page that has testimony, in the browser, since S2581 has no bill document locally. The listing hooks have no unit test of their own: their jest suites already fail at the base commit (see the crumb on #69). Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ient's mockup The conference page gets the client's Public Input feed and Add Your Input panel, built from k-g-k/maple-conference at 19bd101 (the "/" routes, which use its BUILD flag preset). The title carries a "Share your input" button, and Public Input joins the section tabs between Committee and Bill Text. Public Input lists what has been filed as cards, narrowed by account type and position, with a Following filter that appears once the reader follows someone, five to a page, and a modal that opens an entry whole. Each card has the kebab with Follow and Report. The panel writes on the existing draft and publish hooks: autosaved drafts, a review step, and posting, which puts the panel away and scrolls to a "Your Input" section. Editing reopens the panel on the posted text; because the server requires a reason for every republish, the review step shows one Reason for Edit field only then, the one control the mockup does not show. The page is keyed by conference so moving to another starts clean. Left out on purpose: the mockup's weekly-email checkbox (nothing on the server backs it) and its Share action (it has no destination). The followers' digest email and newsfeed card are unchanged and still assume three positions. Closes #69 Verified: site tsc and eslint are clean. jest passes for components/conference (18, including a test that moves between conferences and failed before the page was keyed). The feed card, filter row, and the compose panel and footer were measured against the running mockup and match, bar the differences below. Posting, editing, republishing, following (including from the card menu, which then offers Unfollow), the entry modal and filters were exercised in the browser on the emulator. The server commits below it pass the testimony, ballot question, moderation, conferences and backfill integration suites (59). Not verified: phone width. The review step and the Your Input section were compared by eye only. The feed's headings and filter row do not pin as the mockup's do. The mockup labels the reader's new entry "Just now" and animates the "on the record" line; ours shows the date. The account-type tooltip is a native title. Avatars are initials. The panel sits under our 72px navbar where the mockup's is 60px. The report modal and the unverified-email path were not exercised. The translation files the page loads have no test. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Publishing, and republishing, testimony on a conference that is reportFiled, enacted or dissolved now fails with "not accepting testimony". Deleting stays allowed so people can still take their testimony down. Unlike the ballot question phase, which only the client enforces, this is checked in the publish transaction too, so calling the function directly cannot bypass it. On the site, a closed conference keeps its Public Input but offers no way to write: the title's Share your input button and the Edit item in the author's own card menu only show while it is appointed. The testimony detail page sent conference testimony to the bill's form; it now links to the conference page while the conference is appointed and offers no add or edit after. Closes #78 Verified: the integration tests added here (publish refused for each closed status, republish refused after the status flips, delete allowed on a closed conference) pass. jest covers the closed page (no Share button, Public Input still there), the card menu without Edit on a closed conference, and the detail page's conference link and its absence once reported; the menu test fails with the gate taken out. Site tsc is clean. Not verified: a reportFiled conference in the browser on the emulator. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nce data yarn emulators:start loads tests/integration/exportedTestData, which predates conferences: no conferences, no court-194 members or bills, and test@example.com unverified. Restarting the emulators, or running the integration suite, wipes whatever was added by hand. seedLocal reads conferences/194/*.yaml, queues scraper batches so the functions emulator fetches the real bills and members they name, waits for them, runs syncConferences, verifies test@example.com, and writes seven sample testimonies on one conference. Local only. Verified: on an emulator started clean from the export it filled in 32 bills, 65 members and 16 conferences in about 106 seconds, and every conference page then showed its six conferees. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ewsfeed Conference testimony reaches the author's followers, and both places they see it treated it as bill testimony: the digest email drew a broken position image and linked the bill, and the newsfeed card linked the bill and the bill's testimony page. The digest gives a conference item the conference id and its stance in words, taken from the site's wording (a test holds the two equal). Each of the six slots in the user partial sends it through a new conferenceItem partial that links the conference. The newsfeed card says the author gave input to the conference committee, shows the stance chip from the conference page, and links the conference's Public Input. Bill and ballot-question items are drawn as before. Verified: functions jest src (123), including buildDigestData and a render of the user partial for conference, bill and sixth-slot items; site tsc; NewsfeedCard jest for a conference and a bill card. On the emulator: the test user followed an author from the conference page, a new conference and a new bill testimony by that author both reached the follower's feed, /newsfeed showed both cards with the right links, and the digest built from the emulator rendered the conference item with its stance and conference link beside the bill item's image. Bill rows of the user partial render the same as on main apart from whitespace between tags. Not verified: violet has not looked at the email or the newsfeed in a browser herself, nor has anyone seen the conference stance cell, now 100px wide and wrapping, in an email client. NewsfeedCard's ballot-question test still fails, as it does on main (the test env has ballotQuestions off). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Testimony from before conference testimony has no conferenceId, and Firestore cannot select on a missing field, so every read that kept bill testimony apart from conference testimony fetched the whole bill and filtered in memory. backfillTestimonyScope stamps null on whichever of ballotQuestionId and conferenceId a published, archived or draft testimony lacks (the ballot-question backfill skipped drafts, and this replaces it, in CI too), with --dry-run. Drafts are now written with conferenceId: null. Publish, delete, the counts backfill, the website's draft and publication lookup, the bill's listing and a testimony's version history then filter both fields in the query, with their limits back: publish, delete and the counts backfill are main's code plus one filter, and matchesTestimonyScope goes. The version history fixes a conference filing listing its author's bill versions on the canonical bill, and the reverse. Unscoped listings keep the in-memory filter. Two composite indexes and a conferenceId override are added; the bill listings rely on index merging, as the ballotQuestionId filter already did. The integration test data is migrated with the script. Run per environment, dev then prod: indexes first, then the backfill, then the functions and hosting, then the backfill again for what the old code wrote in between. Stamping after the new code is live hides legacy testimony from its bill and lets a republish duplicate it. Closes #82 Verified: site tsc; functions jest 120/120; yarn test:integration-ci tests/integration components/db, 102 pass, including the backfill on legacy docs and its dry run, republishing backfilled legacy testimony in place beside a conference filing, and delete skipping a newer conference filing for the bill's latest (fails without the filter); the script's dry run, run and second dry run against the exported test data (25 published, 133 archived, 38 drafts, then none left). Not verified: that the indexes serve every query, which the emulator does not check, so only dev shows it; the migration on dev or prod data; the full yarn test:integration-ci, which aborts partway at this commit's parent too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
fadd435 to
8500e3f
Compare
Mephistic
left a comment
There was a problem hiding this comment.
Love this - just a few small questions, nothing that I consider a blocker to getting this PR merged.
|
|
||
| await syncConferences(ctx) | ||
|
|
||
| const user = await auth.getUserByEmail("test@example.com") |
There was a problem hiding this comment.
OOC, if we're relying on fetching an existing test user, why not just set that user to have emailVerified: true in the auth export (or add a new email-verified@example.com test user if we're worried about conflicts with other tests)?
|
|
||
| const want = [ | ||
| ...[...bills].map(id => `generalCourts/${COURT}/bills/${id}`), | ||
| ...[...members].map(id => `generalCourts/${COURT}/members/${id}`) |
There was a problem hiding this comment.
Should this also generate the MemberSearchIndex now that it's powering parts of the Conference feature? IIRC the job that generates the index is a scheduled function (that will never run in the emulators unless explicitly triggered).
| }) | ||
| }) | ||
|
|
||
| it("moves only the total for a position without a field in the set", () => { |
There was a problem hiding this comment.
Just double-checking to make sure I'm caught up on the desired UX: if a bill receives 100 "Public Inputs" and no actual testimony, we expect it to display on its Bill Details page as having 100 Testimonies (with 0 endorse/neutral/oppose counts and no testimony on the page)?
There was a problem hiding this comment.
Great catch. That's not the intended UX at all. It's not what happens today, simply because there's a guard in updateBill that prevents an updateTestimonyCounts from being run with conference testimony on a bill. That said, that check is obviously at the wrong altitude--it needs to live in updateTestimonyCounts. Will fix this.
| items.push( | ||
| <PolicyActionItem | ||
| key="add-testimony" | ||
| billName={`${isUser ? "Edit" : "Add"} Testimony for ${conferenceName( |
There was a problem hiding this comment.
Just double-checking: for a conference, should this be called "Public Input" instead of "Testimony"? Not sure what nomenclature we settled on here.
There was a problem hiding this comment.
Hmm! Yes, the mockups show "Public Input"--I will make the switch & just give the whole thing a consistency pass
| attachmentId: Maybe(String), | ||
| editReason: Maybe(String), | ||
| ballotQuestionId: Maybe(String) | ||
| ballotQuestionId: Maybe(String), |
There was a problem hiding this comment.
Just to clarify: are ballotQuestionId and conferenceId mutually exclusive? If so, should we enforce that constraint in the type definition?
There was a problem hiding this comment.
Yes, great call, let's do this.
| /** The mockup's theme.css values, for the colours the site has no token for. | ||
| * The page's own surfaces and brand come from the --maple tokens, as in | ||
| * ../styles.ts. */ | ||
| export const ink = { |
There was a problem hiding this comment.
@k-g-k 100% not a blocker, but do you know if the goal here to have these tokens be part of the re-branded color palate (or are they unique to the Conference feature)? Just want to make sure we're keeping an eye out for UI consistency here.
|
|
||
| /** The floating surface under a control. `$right` hangs it from the right | ||
| * edge; widths and padding are set by whoever extends it. */ | ||
| export const Menu = styled.div<{ $right?: boolean }>` |
There was a problem hiding this comment.
OOC, what does this custom popover give us that the Popover from react-bootstrap doesn't?
There was a problem hiding this comment.
Nothing; I just am not a UI person and did not realize we already had a Popover, so I didn't catch that Claude had added an unnecessary one. I'll replace it with the react-bootstrap component. Will also take a peek at any other components and make sure they're actually needed.
A fresh emulator plus seedLocal left two gaps. test@example.com was unverified in the shared auth export, so the seed verified it itself; the export now holds it verified and the seed's step goes. The member search index behind the legislator pickers is built only by a scheduled job, which never fires in the emulator, so the pickers were empty; the seed now runs that job's own handler once the scraped members are in. The index holds the members the seed scrapes, the conferees, not the whole House and Senate. The handler runs in the script's process because the emulator's route to scheduled jobs, triggerPubsubFunction, fails on the docker stack, and runScrapers then waits on it forever. Closes #108 Verified: on a fresh docker stack, seedLocal runs clean and memberSearch holds 39 representatives and 26 senators; in Chrome, test@example.com signs in already verified, the edit-profile Representative picker lists names, and the user posts conference input with no profile redirect. Integration suites with the change: testimony, moderation, profile and auth pass on the host; ballotQuestions, backfillTestimonyScope, profile and auth pass in a container, where testimony and moderation fail because its functions emulator does not serve callables. Not verified: CI's integration list in one run on the host. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
What a testimony counts toward was decided in five places: publish and delete each asked again for every target, and the count helper took whatever fields its caller handed it, so a conference position given bill fields bumped the bill's total and none of its stances. Publish and delete now resolve the target once (targetOf: the bill, a ballot question on it, or a conference on it) and make one counts update against it, with the count fields chosen by the target's kind. The helper throws on a position outside those fields: draft validation keeps them out, so one there is a broken invariant. Drafts on both sides, and the site's Testimony, refuse to name both a ballot question and a conference. Delete now skips the counts when the target document is gone, a bill included, as it already did for ballot questions and conferences. Closes #109 Verified: functions tsc and functions testimony jest (14), both re-run after the final cleanup; integration testimony, ballotQuestions and moderation (51/51) on a throwaway emulator, run before that cleanup (delete's target became a local, the helper's field check became a key check), including a refused two-target draft and a delete that fails on a mismatched position without touching the counts; on the docker stack, posting Pass the Senate version on a conference moved its total and stance from 0 to 1 and left its bill's counts at 0. Not verified: bill testimony and admin delete through the browser (the integration suite covers both callables); the integration suite after the final cleanup. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The mockup calls what people file on a conference input, and most of
the conference surfaces already did. Two places still said testimony:
the testimony page's Actions item ("Add/Edit Testimony for ...") and the
report and delete-request modals opened from an entry on the conference
page ("Report Testimony", "submitted on the wrong bill").
The Actions item now reads Add or Edit Your Input for the conference.
The report modal takes an optional title, and the delete-request modal
an optional title, wrong-target reason and rescind text, so the
conference page words both for input while bill and ballot-question
pages keep theirs. The digest's New Testimonies count and the rest of
the testimony page stay as they are.
Closes #110
Verified: PolicyActions jest; conference, TestimonyCard and testimony
jest (56/57, the one failure is ViewTestimony's All Testimonies test,
which fails without this change); site tsc; in Chrome on the local
emulator, the report modal on another person's entry reads Report Input,
and the delete request on your own reads Report Input with the
conference reason and rescind text (checked before the delete request
was folded back into the shared modal).
Not verified: the Actions item in the browser, since the testimony page
crashes on conference input (filed separately); the delete request in
the browser after it moved back into the shared modal; a bill page's
report modal still reading Report Testimony.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The entry kebab, the account-type and position pickers and More Filters ran on a home-made popover that declared menu and listbox roles but neither moved focus nor answered arrow keys. They now run on Dropdown, as the conference picker does: the arrows move through the items, Escape closes and hands focus back to the toggle, and Tab out closes. Every menu is role=menu (the pickers' choices are menuitemradio), since Dropdown is a menu button rather than a listbox. Toggles say they open a menu before the first open, when Dropdown does not yet. Popper does not re-place a menu on scroll, since a menu sits in its control's own flow. Laid beside the mockup, the menus also match where the popover did not: items are weight 500, account-type rows keep the taller line, the position pill's chevron is mid ink, the account menu's first row reads "All Accounts", and on a phone a picker's menu stays under its control and narrows to the room on its right instead of sliding left. The account and position menus still sit about 1.5px nearer their control than the mockup's, which hangs them from a wrapper a little taller than the control itself. Closes #111 Verified: jest components/conference (a new kebab keyboard test included); each menu open and closed beside the mockup at 19bd101, measured at desktop width and in a 390px frame (width, offset, padding, border, radius, shadow, row heights, selected colour); by keyboard in Chrome: open, arrows, Enter, Escape, Tab out, Space in More Filters keeping it open, and the focus ring on its rows. Not verified: with a screen reader; on a real phone or with the viewport resized while a menu is open. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Deployment failed for project maple-dev with the following error: |
27bf668 to
d97c28b
Compare
Mephistic
left a comment
There was a problem hiding this comment.
LGTM - let's try this out on DEV!

People can now file testimony on a committee of conference with a four-way stance on which version should pass, read what others filed in Public Input, and add their own from the Add Your Input panel, while that testimony stays out of the canonical bill's page, counts, search and followers, and reaches the author's followers' digest and newsfeed as conference input. Existing testimony is backfilled with an explicit null conferenceId so bill testimony is filtered by query rather than in memory; the backfill has to run in each environment before and again after the new functions and hosting go live, as the last commit describes.
Closes hyphacoop#69
Closes hyphacoop#82
Closes hyphacoop#78
Closes hyphacoop#108
Closes hyphacoop#109
Closes hyphacoop#110
Closes hyphacoop#111
What's in it
To Deploy
Everything except for the backfill step is automated. The safest way to deploy with no downtime is:
🤖 Generated with Claude Code