-
Notifications
You must be signed in to change notification settings - Fork 0
feat: honor per-activity CFP reopen in the submission edit gate #90
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
e91fc8a
9823e80
ba8dc8a
c1cb9b1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -34,7 +34,11 @@ const PresentationsTable = ({ | |
|
|
||
| const handleEditPresentation = (ev, presentation) => { | ||
| ev.preventDefault(); | ||
| history.push(presentation.getProgressLink()); | ||
| // getProgressLink asks canEdit, so before the first Clock tick it would resolve every | ||
| // reopened presentation to /preview, and the layout's redirect guard makes that one-way. | ||
| // Do nothing until the clock is real rather than navigate somewhere we cannot come back from. | ||
| if (nowUtc == null) return; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @caseylocker The If the time-service request hangs — accepted, never answered, never errored — uicore's The guard itself is right — navigating through // minimal: make the disabled state visible instead of silent
<a onClick={ev => handleEditPresentation(ev, presentation)}
className={nowUtc == null ? "disabled" : ""}
title={nowUtc == null ? T.translate("presentations.clock_syncing") : ""}>{p.title}</a>or simply extend the PR description's Known-limitation paragraph to state that while unsynced the table is non-navigable and statuses are blank, so the tradeoff is on record. Recovery paths already exist (
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Took the documentation option, and you were more right than the comment claimed: the Known limitation paragraph was not just incomplete, it was wrong about the state. It said Your mechanism checks out end to end in the installed bundle: initial The paragraph now records all three symptoms rather than only the frozen window: non-navigable table, with the reason the no-op is deliberate rather than an oversight, and the blank status column. One thing worth adding, because it is also the workaround: the block is only on this navigation path. Left the visible-disabled affordance out. It needs a new string and a disabled style for a state that self-heals on tab re-focus, and you said you would not block on either, so I would rather not add UI for it inside this PR. Happy to take it if you would rather see it. |
||
| history.push(presentation.getProgressLink(nowUtc)); | ||
| }; | ||
|
|
||
| const handleReviewPresentation = (ev, presentation) => { | ||
|
|
||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
@caseylocker Not a change request on this PR — recording a latent crash sitting three lines below the redirect this PR modified, so it doesn't stay undocumented. It is pre-existing: the line is byte-identical on The if (!speaker) {
history.push(`/app/${summit.slug}/all-plans/profile`);
}
Reachability is the open question: Fix is one word — add let { match, entity, speaker, history, loading, location, selectionPlan, selectionPlansSettings, nowUtc, summit } = this.props;Since it predates the branch and is not on a line this PR changes, keeping it out of this PR is the right call — same reasoning as the
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Noted, and agreed it stays out of this PR for the reason you give: it predates the branch and is not on a line this PR touches. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -39,7 +39,6 @@ class Presentation { | |
| this._presentation.selectionPlan = summit.selection_plans.find(sp => sp.id === presentation.selection_plan_id); | ||
| this._tagGroups = tagGroups; | ||
| this._track = null; | ||
| this._submissionIsClosed = selectionPlan ? !nowBetween(selectionPlan.submission_begin_date, selectionPlan.submission_end_date) : true; | ||
|
|
||
| this._steps = [ | ||
| {name: 'NEW', lcName: 'new', step: 0}, | ||
|
|
@@ -77,11 +76,21 @@ class Presentation { | |
| this._presentation.progressNum = currentStep.step; | ||
| } | ||
|
|
||
| // the plan is captured at construction, but navigating refetches it by id and swaps the copy | ||
| // held in redux, so a long-lived instance has to be told or canEdit keeps gating on the old one | ||
| updateSelectionPlan(selectionPlan) { | ||
| this._selectionPlan = selectionPlan; | ||
| } | ||
|
smarcet marked this conversation as resolved.
|
||
|
|
||
| /** | ||
| * @param nowUtc | ||
| * @returns {React.ReactNode} | ||
| */ | ||
| getStatus(nowUtc) { | ||
| // every branch below classifies the submission and selection windows against nowUtc, so | ||
| // before the first Clock tick there is no answer to give. null coerces to 0 in these | ||
| // comparisons, which would render a confidently wrong status; render nothing instead. | ||
| if (nowUtc == null) return null; | ||
|
|
||
| const {is_published, status, selection_status, selectionPlan} = this._presentation; | ||
| const { | ||
|
|
@@ -146,8 +155,57 @@ class Presentation { | |
| return (this._presentation.is_published || this._presentation.status === 'Received'); | ||
| } | ||
|
|
||
| canEdit() { | ||
| if (!this._selectionPlan || this._submissionIsClosed) return false; | ||
| /** | ||
| * The operative reopen deadline, or null when a grant is not what is letting this | ||
| * presentation be edited. Four things must hold, mirroring the API's | ||
| * isSubmissionReopened(): the plan is enabled, it has a submission end date, that window | ||
| * has actually ENDED, and the grant is still live. "Not open" is not the same as "ended" — | ||
| * the window is also not open before it starts, and honoring a grant there would admit | ||
| * edits the API refuses. | ||
| * | ||
| * Single definition on purpose: canEdit() gates on it and the banner displays it, and if | ||
| * the two drifted the banner would announce a deadline that does not constrain anything — | ||
| * e.g. after an admin extends submission_end_date past an existing grant. | ||
| * | ||
| * @param nowUtc epoch seconds from the Clock, or null before the first tick | ||
| * @returns {number|null} | ||
| */ | ||
| getReopenedUntil(nowUtc) { | ||
| if (nowUtc == null) return null; | ||
| if (!this._selectionPlan || this._selectionPlan.is_enabled === false) return null; | ||
| // ungranted arrives as null on the list feeds and '' on the detail feed, which coerces | ||
| // every null to empty string; a falsy check covers both | ||
| const until = this._presentation.submission_reopened_until; | ||
| if (!until) return null; | ||
| // no end date means no window to have ended, so there is nothing to reopen. Without this | ||
| // the comparison below is nowUtc <= 0 (null and '' coerce, undefined gives NaN), which is | ||
| // false, so the grant would be honored and the form would render against a plan every | ||
| // write fails on. Falsy check, matching the coercion note above. | ||
| if (!this._selectionPlan.submission_end_date) return null; | ||
| if (nowUtc <= this._selectionPlan.submission_end_date) return null; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @caseylocker
Reachability is admittedly thin: the reopen endpoint refuses to stamp a grant on a plan without an end date ( if (!this._selectionPlan.submission_end_date) return null;placed before the
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Confirmed and fixed in Two things I found verifying it that make this worse than the write-refusal you described. It gates edit access, not just the banner. The refusal is a 500, not a validation error. On reachability, which you flagged as unverified: No test: this repo has no test harness at all, no jest and no test script, so there is nothing to add without introducing one. Verified by reasoning through the three falsy shapes plus ended-and-live, not-yet-ended and expired, and |
||
| return nowUtc < until ? until : null; | ||
| } | ||
|
|
||
| /** | ||
| * @param nowUtc epoch seconds, server-synced via the Clock | ||
| * @returns {boolean} | ||
| */ | ||
| canEdit(nowUtc) { | ||
| if (!this._selectionPlan) return false; | ||
| // the API refuses writes on a disabled plan, and a disabled one does reach client state: | ||
| // selection-plan-layout refetches the plan by id on navigation, that endpoint applies no | ||
| // enabled filter unlike the /me feed, and base-reducer swaps the filtered copy for it. | ||
| // Only an explicit false blocks, so a payload without the field still edits normally. | ||
| if (this._selectionPlan.is_enabled === false) return false; | ||
|
|
||
| // computed per call, not snapshotted in the constructor, so a window that ends while the | ||
| // page is open locks the form without a reload. Still on nowBetween's local clock, as it | ||
| // was before this feature; the reopen check below is the part that moved to the synced | ||
| // one, because a 24h grant makes skew a far larger fraction than a multi-week plan window. | ||
| const submissionIsClosed = !nowBetween(this._selectionPlan.submission_begin_date, this._selectionPlan.submission_end_date); | ||
| const reopened = !!this.getReopenedUntil(nowUtc); | ||
|
|
||
| if (submissionIsClosed && !reopened) return false; | ||
|
|
||
| let speakers = this._presentation.speakers.map(s => { | ||
| if (typeof s == 'object') return s.id; | ||
|
|
@@ -172,9 +230,9 @@ class Presentation { | |
| return (!this._presentation.is_published && belongsToSP); | ||
| } | ||
|
|
||
| getProgressLink() { | ||
| getProgressLink(nowUtc) { | ||
|
|
||
| if (this.canEdit()) { | ||
| if (this.canEdit(nowUtc)) { | ||
|
|
||
| let step = 'summary'; | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
src/components/presentations-table.js:93— outside this PR's diff hunk, so GitHub won't let me anchor to it; file-level comment instead.@caseylocker Not a change request on this PR — recording the Delete-button defect here so the analysis lives next to the code rather than only in the Copilot thread that got resolved.
The guard on the Delete button is dead, and has been since it was written:
Presentationhas never defined asubmissionIsClosedmember. Onmasterthe class only declared the underscore-prefixed private field (presentation.js:42), read internally bycanEdit()(:150); there is no getter and no assignment to the un-prefixed name anywhere in the file. Sopresentation.submissionIsClosedisundefined,!undefinedistrue, and the first term is a constant.That leaves the condition as
canDelete()alone (presentation.js:198-203), which checks!is_publishedandselection_plan_id === selectionPlan.idand nothing time-related. The Delete button therefore renders on any unpublished presentation of the plan, including long after the submission window closed.No data is at risk — the server does enforce it.
PresentationService::deletePresentation(summit-api:590-592):So the user-visible defect is a destructive action offered in the UI that always fails once the window is closed.
Confirming your read in the resolved Copilot thread: this PR did not cause it. Removing
_submissionIsClosedchanged nothing, because nothing outside the class ever read it. Your call to leave it alone here is right — the fix belongs incanDelete(), and it would change delete behaviour for every submission rather than for reopened ones:presentations-table.jsalready hasnowUtcin scope (mapStateToProps:121), so the call site becomes{presentation.canDelete(nowUtc) && …}. Worth its own ticket so it gets tested on its own terms.