diff --git a/src/components/forms/__tests__/event-form.test.js b/src/components/forms/__tests__/event-form.test.js
index 81b730f59..076ed9ba4 100644
--- a/src/components/forms/__tests__/event-form.test.js
+++ b/src/components/forms/__tests__/event-form.test.js
@@ -56,7 +56,8 @@ describe("EventForm", () => {
moderator: null,
sponsors: [],
tags: [],
- extra_questions: []
+ extra_questions: [],
+ materials: []
},
errors: {},
onSubmit: jest.fn(),
@@ -82,8 +83,16 @@ describe("EventForm", () => {
onClone: jest.fn()
};
- const renderEventForm = (overrides = {}) =>
- render();
+ // 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/, {
+ 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 +126,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 +200,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 +407,22 @@ describe("EventForm", () => {
).not.toBeInTheDocument();
});
+ it("announces the deadline on the collapsed Materials panel title", () => {
+ renderEventForm({ entity: grantedEntity });
+
+ // Collapsed is the point: expanded passes even if gated on showSection.
+ 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"
+ })
+ ).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..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,6 +759,18 @@ class EventForm extends React.Component {
return moment().unix() > plan.submission_end_date;
}
+ // 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 (
+ this.isPresentation() &&
+ !this.isNew() &&
+ entity.selection_plan_id > 0 &&
+ this.isReopenApplicable()
+ );
+ }
+
isSubmissionReopened() {
const deadline = this.getReopenDeadline();
return !!deadline?.isAfter(moment());
@@ -782,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
@@ -1259,126 +1273,6 @@ class EventForm extends React.Component {