Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions components/conference/ConferenceDetails.switching.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ jest.mock("next/router", () => ({
useRouter: () => ({ asPath: "/conferences/194/a" })
}))
jest.mock("usehooks-ts", () => ({ useMediaQuery: () => false }))
// react-markdown ships ESM only, and the gist here is empty.
jest.mock("react-markdown", () => ({ __esModule: true, default: () => null }))
jest.mock("../featureFlags", () => ({
useFlags: () => ({ legislators: false, hearingsAndTranscriptions: true })
}))
Expand Down
36 changes: 36 additions & 0 deletions components/conference/ConferenceDetails.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,29 @@ const routeChangeStart = (url: string) =>
jest.mocked(Router.events.on).mock.calls[0][1](url)

jest.mock("usehooks-ts", () => ({ useMediaQuery: () => false }))
// react-markdown ships ESM only. This reads one link, as the gist writes it.
jest.mock("react-markdown", () => ({
__esModule: true,
default: ({
children,
components
}: {
children: string
components?: { a?: React.ComponentType<any> }
}) => {
const match = children.match(/\[([^\]]+)\]\(([^)]+)\)/)
const Link = components?.a ?? "a"
return match ? (
<p>
{children.slice(0, match.index)}
<Link href={match[2]}>{match[1]}</Link>
{children.slice((match.index ?? 0) + match[0].length)}
</p>
) : (
<p>{children}</p>
)
}
}))

let mockLegislators = false
jest.mock("../featureFlags", () => ({
Expand Down Expand Up @@ -607,6 +630,19 @@ describe("ConferenceDetails", () => {
expect(screen.getByText("Bans use.")).toBeInTheDocument()
})

it("links a markdown link in the gist, out to a new tab", () => {
render(
<ConferenceDetails
{...props}
gist="Voters passed [Question 1](https://example.org/q1) in November."
/>
)
const link = screen.getByRole("link", { name: "Question 1" })
expect(link).toHaveAttribute("href", "https://example.org/q1")
expect(link).toHaveAttribute("target", "_blank")
expect(link).toHaveAttribute("rel", "noopener noreferrer")
})

it("opens a term's note on hover", () => {
render(<ConferenceDetails {...props} />)
fireEvent.pointerEnter(screen.getByText("Chapter 93M"))
Expand Down
28 changes: 26 additions & 2 deletions components/conference/ConferenceDifferences.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Fragment, KeyboardEvent, useRef, useState } from "react"
import { Trans, useTranslation } from "next-i18next"
import ReactMarkdown from "react-markdown"
import styled from "styled-components"
import { FEEDBACK_URL } from "../common"
import { shortName } from "../formatting"
Expand Down Expand Up @@ -80,13 +81,22 @@ const Gist = styled.div`
line-height: 1.65;
color: ${ink.mid};
}
p + p {
/* The feedback line beneath the gist. */
> div + p {
margin-top: 8px;
font-size: 0.8125rem;
color: ${ink.faint};
}
`

/** The gist's own text, written as markdown, so its paragraphs are told
* apart from the feedback line. */
const GistText = styled.div`
p + p {
margin-top: 8px;
}
`

const DottedLink = styled.a`
font-weight: 600;
color: var(--maple-brand-primary);
Expand Down Expand Up @@ -378,7 +388,21 @@ export function ConferenceDifferences({
<Body ref={body}>
{gist && (
<Gist>
<p>{gist}</p>
<GistText>
<ReactMarkdown
components={{
a: ({ node: _node, ...props }) => (
<DottedLink
{...props}
target="_blank"
rel="noopener noreferrer"
/>
)
}}
>
{gist}
</ReactMarkdown>
</GistText>
<p>
<Trans
t={t}
Expand Down
2 changes: 1 addition & 1 deletion components/conference/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ export type ConferencePageProps = {
conferences: ConferenceSummary[]
conferees: ConfereeView[]
hearings: HearingView[]
/** What the bill is, in one sentence; null where none is written. */
/** What the bill is, in markdown; null where none is written. */
gist: string | null
differences: DifferenceView[]
similarities: SimilarityView[]
Expand Down
9 changes: 6 additions & 3 deletions conferences/194/public-records.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@ id: public-records
name: Public Records
court: 194
title: An Act promoting transparency and public access in state government
gist: Both bills would let the public request listed categories of the Legislature's records through a separate process run by a records officer in each chamber, and both bring the Governor's office under the public records law for records from January 2027 on. They split on how courts review denials, whether the State Auditor may audit the Legislature (House only), which exemptions apply, and a journalist shield law (Senate only).
gist: |-
Both bills would let the public request a set list of the Legislature's records through a separate process run by a records officer in each chamber, and both would bring the Governor's office under the public records law. They differ most on how courts review denials, whether the State Auditor may audit the Legislature (House only), which exemptions apply, and a journalist shield law (Senate only).

Both bills were written before voters approved [Question 1](https://www.sec.state.ma.us/divisions/elections/research-and-statistics/2026-Information-for-voters/2026-ballot-question1.htm) on Nov. 3, 2026. Starting Jan. 1, 2027, that law puts the Legislature and the Governor's office under the general public records law, with new exemptions for constituent casework and for documents about developing policy. A final bill based on either version would replace that approach for the Legislature's records.
senateBillId: S3244
houseBillId: H5469
canonicalBillId: H5469
Expand Down Expand Up @@ -50,8 +53,8 @@ differences:
- heading: Should the bill include money to carry out the new rules?
house: "Yes, $1,000,000 for fiscal year 2026: $250,000 each for the Governor's office, the Senate, the House and joint legislative operations. That fiscal year has ended. (§§1–2)"
similarities:
- heading: Would the Legislature have to answer public records requests?
body: Yes, but only for a set list of final legislative records, through its own process. That process is the only way to get them; the general public records law would not apply to the Legislature.
- heading: How would requests for the Legislature's records work?
body: Only a set list of final legislative records would be available, through the Legislature's own process. That process would be the only way to get them; the general public records law would not apply to the Legislature.
- heading: Which Legislature records would be covered?
body: Bills, amendments, summaries and fiscal notes, with their sponsors; hearing notices, attendance and recordings or transcripts; written testimony (subject to committee rules on sensitive information); committee votes and reports; rules, calendars, journals and roll calls; leadership, committee and office assignments; employee manuals and policies; appointment notices; reports required by law; spending records sent to the Comptroller, including salaries and payments to vendors; final internal audits; ethics disclosures; the State House press list; and approved district maps.
- heading: Who would handle requests?
Expand Down
2 changes: 1 addition & 1 deletion docs/updating-conferences.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ This drafts each conference that doesn't have a file yet. Finish each draft by h
1. Resolve every `TODO` (conferee member codes, the partner bill).
2. Check `status` and `reportBillId`. The script reads the report bill's history for enactment, but only if that bill has been fetched (see Local data).
3. Add `hearingIds` (`hearing-{EventId}`) from the hearing list the script prints.
4. Write the `gist` (a short paragraph), then `differences` and `similarities`. A `source` is optional and isn't shown yet. Leave out a chamber's text when its bill doesn't touch the point.
4. Write the `gist` (a short paragraph; markdown links and blank-line paragraph breaks work), then `differences` and `similarities`. A `source` is optional and isn't shown yet. Leave out a chamber's text when its bill doesn't touch the point.

## Updating a conference

Expand Down
3 changes: 2 additions & 1 deletion functions/src/conferences/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,8 @@ export const Conference = withDefaults(
/** A short name for headings and lists ("Phone-Free Schools"), as the
* press refers to it. Pages fall back to the title. */
name: Optional(String),
/** What the bills are, in a short paragraph, above what is unresolved. */
/** What the bills are, in a short paragraph of markdown, above what is
* unresolved. */
gist: Optional(String),
senateBillId: Id,
houseBillId: Id,
Expand Down
Loading