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 {

)} - {this.isPresentation() && - !this.isNew() && - entity.selection_plan_id > 0 && - this.isReopenApplicable() && ( -
-
- - {!this.isSubmissionReopened() && ( -
- - - {reopenHours === "custom" && ( - <> - - - this.setState({ - reopenCustomHours: ev.target.value - }) - } - /> - - )} - -
- )} - {this.isSubmissionReopened() && ( -
- - {T.translate("edit_event.reopened_until", { - deadline: this.getReopenDeadline().format( - REOPEN_DEADLINE_FORMAT - ) - })} - - {entity.submission_reopened_by && ( - - {T.translate("edit_event.reopened_by", { - admin: `${entity.submission_reopened_by.first_name} ${entity.submission_reopened_by.last_name}` - })} - - )} - - {window.CFP_APP_BASE_URL && ( - - -   - -   - {speakerDeepLink} - - )} -
- )} -
-
- )}
  @@ -2073,8 +1967,9 @@ class EventForm extends React.Component { {entity.id != 0 && this.isEventType(EVENT_TYPE_PRESENTATION) && ( +
+ )} + {this.isSubmissionReopened() && ( +
+ + {T.translate("edit_event.reopened_until", { + deadline: this.getReopenDeadline().format( + REOPEN_DEADLINE_FORMAT + ) + })} + + {entity.submission_reopened_by && ( + + {T.translate("edit_event.reopened_by", { + admin: `${entity.submission_reopened_by.first_name} ${entity.submission_reopened_by.last_name}` + })} + + )} + + {window.CFP_APP_BASE_URL && ( + + +   + +   + {speakerDeepLink} + + )} +
+ )} +
+ + )} )} 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",