From abb724b7829f078a4f3eae2dc26a16eab0bd2aff Mon Sep 17 00:00:00 2001
From: Casey Locker
Date: Fri, 21 Aug 2026 08:53:34 -0500
Subject: [PATCH 1/3] refactor(event-form): move the CFP reopen section into
the Materials panel
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
The reopen control sat at the top of the activity form, above Submitter,
where it was visible but not grouped with the thing it governs. Moved it
inside the Materials panel, below the materials table.
The panels are a single open accordion (showSection starts at "main"), so
the panel is collapsed on load and the move would otherwise hide whether a
talk has a live window. The panel title carries that state instead:
Materials
Materials — submission reopened until August 22, 2026 6:51 am
Title and section body share one gate, isReopenSectionVisible(). Keying the
title on isSubmissionReopened() alone would announce a deadline in exactly
the case the comment on isReopenApplicable warns about: a live grant whose
plan window was since extended, which the server no longer honours. There
is already a test for that case and it now covers the title too.
Also:
* Panel gets an explicit id="materials". uicore's Panel derives its DOM id
from the title when none is passed, so a dynamic title would otherwise
move the id every time a grant opens or closes.
* Drops the "CFP submission window" label inside the section. The panel
title already names the group, and two headings stacked read as noise.
* renderEventForm in the test file expands the panel before asserting,
since uicore's Panel mounts children only while expanded. queryByText,
not getByText, so the tests that assert the control is absent still
exercise a real absence rather than passing on a collapsed panel.
* Test fixture gains materials: [], which the Table now actually maps over.
Verified in the browser against api.dev.fnopen.com on summit 13 / activity
3535: both title states, the collapsed header, and the explicit panel id.
Co-Authored-By: Claude
---
.../forms/__tests__/event-form.test.js | 42 ++-
src/components/forms/event-form.js | 259 ++++++++++--------
src/i18n/en.json | 2 +-
3 files changed, 178 insertions(+), 125 deletions(-)
diff --git a/src/components/forms/__tests__/event-form.test.js b/src/components/forms/__tests__/event-form.test.js
index 81b730f59..601faf683 100644
--- a/src/components/forms/__tests__/event-form.test.js
+++ b/src/components/forms/__tests__/event-form.test.js
@@ -56,7 +56,10 @@ describe("EventForm", () => {
moderator: null,
sponsors: [],
tags: [],
- extra_questions: []
+ extra_questions: [],
+ // The Materials panel is expanded by renderEventForm, so uicore's Table now
+ // actually mounts and maps over this.
+ materials: []
},
errors: {},
onSubmit: jest.fn(),
@@ -82,8 +85,18 @@ describe("EventForm", () => {
onClone: jest.fn()
};
- const renderEventForm = (overrides = {}) =>
- render();
+ // The reopen section lives inside the Materials panel, and uicore's Panel mounts
+ // its children only while expanded, so every assertion here needs it open first.
+ // queryByText, not getByText: the panel itself is gated on a saved presentation,
+ // and the tests that assert the control is absent pass entities that suppress it.
+ const renderEventForm = (overrides = {}) => {
+ const result = render();
+ const materialsHeading = screen.queryByText(/^edit_event\.materials/, {
+ selector: ".panel-title"
+ });
+ if (materialsHeading) fireEvent.click(materialsHeading);
+ return result;
+ };
// Built on baseProps.entity (not the bare object from the task brief) because
// several unrelated, pre-existing render paths (TagInput, isEventType,
@@ -117,6 +130,14 @@ describe("EventForm", () => {
).toBeInTheDocument();
});
+ it("leaves the Materials panel title plain when there is no grant", () => {
+ renderEventForm({ entity: baseEntity });
+
+ expect(
+ screen.getByText("edit_event.materials", { selector: ".panel-title" })
+ ).toBeInTheDocument();
+ });
+
it("does not offer the reopen control on a new presentation", () => {
renderEventForm({ entity: { ...baseEntity, id: 0 } });
@@ -183,6 +204,11 @@ describe("EventForm", () => {
expect(
screen.queryByText(/edit_event.reopened_until/)
).not.toBeInTheDocument();
+ // The panel title shares the section's gate for exactly this case: keyed on the
+ // grant alone it would announce a deadline the server no longer honours.
+ expect(
+ screen.getByText("edit_event.materials", { selector: ".panel-title" })
+ ).toBeInTheDocument();
});
it("disables the reopen button when no valid hours value is selected", async () => {
@@ -385,6 +411,16 @@ describe("EventForm", () => {
).not.toBeInTheDocument();
});
+ it("announces the deadline on the collapsed Materials panel title", () => {
+ renderEventForm({ entity: grantedEntity });
+
+ expect(
+ screen.getByText("edit_event.materials_reopened", {
+ selector: ".panel-title"
+ })
+ ).toBeInTheDocument();
+ });
+
it("still shows the reopened state when the payload was not expanded", () => {
renderEventForm({ entity: grantedUnexpandedEntity });
diff --git a/src/components/forms/event-form.js b/src/components/forms/event-form.js
index 12c503091..9ac5a77ea 100644
--- a/src/components/forms/event-form.js
+++ b/src/components/forms/event-form.js
@@ -766,6 +766,20 @@ class EventForm extends React.Component {
return moment().unix() > plan.submission_end_date;
}
+ // The panel title and the section body MUST share one gate. Keying the title on
+ // isSubmissionReopened() alone would announce a deadline in exactly the case the
+ // comment on isReopenApplicable describes: a live grant whose plan window was
+ // since extended, which the server no longer treats as operative.
+ isReopenSectionVisible() {
+ const { entity } = this.state;
+ return (
+ this.isPresentation() &&
+ !this.isNew() &&
+ entity.selection_plan_id > 0 &&
+ this.isReopenApplicable()
+ );
+ }
+
isSubmissionReopened() {
const deadline = this.getReopenDeadline();
return !!deadline?.isAfter(moment());
@@ -1259,126 +1273,6 @@ class EventForm extends React.Component {
+ )}
)}
diff --git a/src/i18n/en.json b/src/i18n/en.json
index d8ff57b56..49357a913 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -640,6 +640,7 @@
"event_cloned": "Activity cloned successfully.",
"proximity_alert": "Speaker/s have sessions 90min before or after:",
"materials": "Materials",
+ "materials_reopened": "Materials — submission reopened until {deadline}",
"add_material": "Add Material",
"display_on_site": "Display on site",
"type": "Type",
@@ -714,7 +715,6 @@
"save_and_mark_complete": "Save & Mark Complete",
"event_saved_as_draft": "Activity saved as draft successfully.",
"reopen_submission": "Reopen submission",
- "reopen_submission_section": "CFP submission window",
"reopen_duration": "Reopen for",
"reopen_duration_24": "24 hours",
"reopen_duration_48": "48 hours",
From e149743a7a003994ae5ef5d9d8566d0541e96527 Mon Sep 17 00:00:00 2001
From: Casey Locker
Date: Fri, 21 Aug 2026 09:18:52 -0500
Subject: [PATCH 2/3] test(event-form): assert the reopened title while the
panel is collapsed
Copilot review on #1047. The test named "announces the deadline on the
collapsed Materials panel title" never collapsed the panel, because
renderEventForm expands Materials for every test in the file.
That is the one behaviour the move exists to preserve, and it had no
coverage. Gating the title on showSection, which would blank the header
exactly when an operator needs it, passed all 29 tests.
Collapse again after render, and assert the panel body is unmounted so the
test cannot drift back to asserting the expanded state. Verified: it passes
against current code and fails against that mutation.
Co-Authored-By: Claude
---
src/components/forms/__tests__/event-form.test.js | 9 +++++++++
1 file changed, 9 insertions(+)
diff --git a/src/components/forms/__tests__/event-form.test.js b/src/components/forms/__tests__/event-form.test.js
index 601faf683..e19b865ab 100644
--- a/src/components/forms/__tests__/event-form.test.js
+++ b/src/components/forms/__tests__/event-form.test.js
@@ -414,6 +414,15 @@ describe("EventForm", () => {
it("announces the deadline on the collapsed Materials panel title", () => {
renderEventForm({ entity: grantedEntity });
+ // renderEventForm leaves the panel open. Collapse it again: the header
+ // reading without expanding is the whole point of it, so asserting while
+ // expanded passes even when the title is gated on showSection. Asserting
+ // the body is unmounted keeps it that way.
+ fireEvent.click(
+ screen.getByText(/^edit_event\.materials/, { selector: ".panel-title" })
+ );
+
+ expect(document.querySelector("#materials .panel-body")).toBeNull();
expect(
screen.getByText("edit_event.materials_reopened", {
selector: ".panel-title"
From 7692c571485cc998a5e71edba67f0a71c47f3c04 Mon Sep 17 00:00:00 2001
From: Casey Locker
Date: Fri, 21 Aug 2026 10:22:14 -0500
Subject: [PATCH 3/3] refactor(event-form): extract the panel title, trim the
comments
Reviewer notes on #1047.
Extract the Materials panel title into getMaterialsPanelTitle(), alongside
the other reopen getters. Pulls a nested ternary out of a 2500-line render
and matches the class's existing getReopenDeadline/getMaxReopenHours shape.
The gate it shares with the section body is now held by the collapsed-title
test rather than by proximity: mutation-checked by gating the method on
showSection, which fails that test and nothing else.
Trim four comment blocks. event-form.js on master carries no multi-line
comment blocks in 2460 lines, and all four were new here. Kept the two
non-obvious claims: entity-from-state in isReopenApplicable, and
queryByText-not-getByText in the test helper.
169 suites / 1510 tests pass. eslint: 0 errors, same 327 pre-existing
warnings as master.
Co-Authored-By: Claude
---
.../forms/__tests__/event-form.test.js | 13 ++-----
src/components/forms/event-form.js | 36 ++++++++-----------
2 files changed, 17 insertions(+), 32 deletions(-)
diff --git a/src/components/forms/__tests__/event-form.test.js b/src/components/forms/__tests__/event-form.test.js
index e19b865ab..076ed9ba4 100644
--- a/src/components/forms/__tests__/event-form.test.js
+++ b/src/components/forms/__tests__/event-form.test.js
@@ -57,8 +57,6 @@ describe("EventForm", () => {
sponsors: [],
tags: [],
extra_questions: [],
- // The Materials panel is expanded by renderEventForm, so uicore's Table now
- // actually mounts and maps over this.
materials: []
},
errors: {},
@@ -85,10 +83,8 @@ describe("EventForm", () => {
onClone: jest.fn()
};
- // The reopen section lives inside the Materials panel, and uicore's Panel mounts
- // its children only while expanded, so every assertion here needs it open first.
- // queryByText, not getByText: the panel itself is gated on a saved presentation,
- // and the tests that assert the control is absent pass entities that suppress it.
+ // Panel mounts children only while expanded. queryByText, not getByText: the
+ // absence tests pass entities that suppress the panel itself.
const renderEventForm = (overrides = {}) => {
const result = render();
const materialsHeading = screen.queryByText(/^edit_event\.materials/, {
@@ -414,10 +410,7 @@ describe("EventForm", () => {
it("announces the deadline on the collapsed Materials panel title", () => {
renderEventForm({ entity: grantedEntity });
- // renderEventForm leaves the panel open. Collapse it again: the header
- // reading without expanding is the whole point of it, so asserting while
- // expanded passes even when the title is gated on showSection. Asserting
- // the body is unmounted keeps it that way.
+ // Collapsed is the point: expanded passes even if gated on showSection.
fireEvent.click(
screen.getByText(/^edit_event\.materials/, { selector: ".panel-title" })
);
diff --git a/src/components/forms/event-form.js b/src/components/forms/event-form.js
index 9ac5a77ea..08ff6f8d1 100644
--- a/src/components/forms/event-form.js
+++ b/src/components/forms/event-form.js
@@ -745,15 +745,8 @@ class EventForm extends React.Component {
return entity.class_name === "Presentation";
}
- // The API's isSubmissionReopened() requires three things: the plan enabled, its
- // submission window actually ended, and a live grant. Keying the UI on the grant
- // alone lets it announce a deadline the server no longer treats as operative --
- // e.g. an admin grants a reopen, then extends the plan's submission_end_date past
- // it, and the speaker is editing under normal open-window rules again.
- // entity comes from state, not props, because that is what the render gate reads.
- // handleChangeSelectionPlan writes selection_plan_id into state without saving, and
- // componentDidUpdate only syncs the other way, so reading props here would judge
- // eligibility against the persisted plan while the form displays a different one.
+ // entity from state, not props: handleChangeSelectionPlan writes the plan into
+ // state without saving, so props would judge a plan the form is not showing.
isReopenApplicable() {
const { selectionPlansOpts } = this.props;
const { entity } = this.state;
@@ -766,10 +759,8 @@ class EventForm extends React.Component {
return moment().unix() > plan.submission_end_date;
}
- // The panel title and the section body MUST share one gate. Keying the title on
- // isSubmissionReopened() alone would announce a deadline in exactly the case the
- // comment on isReopenApplicable describes: a live grant whose plan window was
- // since extended, which the server no longer treats as operative.
+ // Title and section body must share this gate: a grant whose plan window was
+ // since extended is no longer the operative deadline.
isReopenSectionVisible() {
const { entity } = this.state;
return (
@@ -796,6 +787,15 @@ class EventForm extends React.Component {
);
}
+ getMaterialsPanelTitle() {
+ if (!this.isReopenSectionVisible() || !this.isSubmissionReopened()) {
+ return T.translate("edit_event.materials");
+ }
+ return T.translate("edit_event.materials_reopened", {
+ deadline: this.getReopenDeadline().format(REOPEN_DEADLINE_FORMAT)
+ });
+ }
+
// Mirrors the server's CFP_MAX_REOPEN_HOURS so an over-ceiling value is caught
// before the confirm dialog rather than by the 412 after it. dotenv values are
// strings, hence the coercion. Unset means uncapped: the server's 412 stays the
@@ -1969,15 +1969,7 @@ class EventForm extends React.Component {