-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #4170 from gemini-hlsw/sc-3153-enforce-proposal-de…
…adlines Enforce proposal deadlines
- Loading branch information
Showing
6 changed files
with
229 additions
and
134 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
51 changes: 51 additions & 0 deletions
51
explore/src/main/scala/explore/SubmittedProposalMessage.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
// Copyright (c) 2016-2023 Association of Universities for Research in Astronomy, Inc. (AURA) | ||
// For license information see LICENSE or https://opensource.org/licenses/BSD-3-Clause | ||
|
||
package explore | ||
|
||
import cats.effect.IO | ||
import cats.syntax.all.* | ||
import crystal.react.hooks.* | ||
import explore.model.Proposal | ||
import fs2.Stream | ||
import japgolly.scalajs.react.* | ||
import japgolly.scalajs.react.vdom.html_<^.* | ||
import lucuma.core.model.ProposalReference | ||
import lucuma.core.util.Timestamp | ||
import lucuma.react.common.ReactFnProps | ||
import lucuma.react.primereact.Message | ||
|
||
import scala.concurrent.duration.* | ||
|
||
case class SubmittedProposalMessage( | ||
proposalReference: Option[ProposalReference], | ||
deadline: Option[Timestamp] | ||
) extends ReactFnProps(SubmittedProposalMessage.component): | ||
private val proposalReferenceStr: String = | ||
proposalReference.map(pr => s" as ${pr.label}").orEmpty | ||
|
||
private val deadlineStr: String = | ||
deadline | ||
.map(d => s" until the proposal deadline at ${Proposal.deadlineString(d)}") | ||
.orEmpty | ||
|
||
object SubmittedProposalMessage: | ||
private type Props = SubmittedProposalMessage | ||
|
||
private val component = | ||
ScalaFnComponent | ||
.withHooks[Props] | ||
.useStreamOnMount: | ||
Stream | ||
.fixedRateStartImmediately[IO](1.second) | ||
.evalMap: _ => | ||
IO.monotonic.map(finiteDuration => Timestamp.ofEpochMilli(finiteDuration.toMillis)) | ||
.render: (props, nowPot) => | ||
val retractStr: String = nowPot.toOption.flatten | ||
.filter(now => props.deadline.forall(_ > now)) | ||
.as(s" and may be retracted${props.deadlineStr}") | ||
.orEmpty | ||
|
||
Message(text = | ||
s"The proposal has been submitted${props.proposalReferenceStr}${retractStr}." | ||
) |
43 changes: 0 additions & 43 deletions
43
explore/src/main/scala/explore/proposal/CallDeadline.scala
This file was deleted.
Oops, something went wrong.
153 changes: 153 additions & 0 deletions
153
explore/src/main/scala/explore/proposal/ProposalSubmissionBar.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,153 @@ | ||
// Copyright (c) 2016-2023 Association of Universities for Research in Astronomy, Inc. (AURA) | ||
// For license information see LICENSE or https://opensource.org/licenses/BSD-3-Clause | ||
|
||
package explore.proposal | ||
|
||
import cats.effect.IO | ||
import cats.syntax.all.* | ||
import clue.FetchClient | ||
import clue.ResponseException | ||
import clue.data.syntax.* | ||
import crystal.* | ||
import crystal.react.* | ||
import crystal.react.hooks.* | ||
import explore.DefaultErrorPolicy | ||
import explore.components.ui.ExploreStyles | ||
import explore.model.AppContext | ||
import explore.model.Proposal | ||
import explore.syntax.ui.* | ||
import fs2.Stream | ||
import japgolly.scalajs.react.* | ||
import japgolly.scalajs.react.vdom.html_<^.* | ||
import lucuma.core.model.CallForProposals | ||
import lucuma.core.model.Program | ||
import lucuma.core.util.NewType | ||
import lucuma.core.util.Timestamp | ||
import lucuma.react.common.ReactFnProps | ||
import lucuma.react.primereact.Button | ||
import lucuma.react.primereact.Message | ||
import lucuma.react.primereact.Tag | ||
import lucuma.react.primereact.Toolbar | ||
import lucuma.schemas.ObservationDB | ||
import lucuma.schemas.ObservationDB.Types.SetProposalStatusInput | ||
import lucuma.schemas.enums.ProposalStatus | ||
import lucuma.ui.primereact.* | ||
import lucuma.ui.reusability.given | ||
import org.typelevel.log4cats.Logger | ||
import queries.common.ProposalQueriesGQL.SetProposalStatus | ||
|
||
import scala.concurrent.duration.* | ||
|
||
case class ProposalSubmissionBar( | ||
programId: Program.Id, | ||
proposalStatus: View[ProposalStatus], | ||
deadline: Option[Timestamp], | ||
callId: Option[CallForProposals.Id], | ||
isStdUser: Boolean | ||
) extends ReactFnProps(ProposalSubmissionBar.component) | ||
|
||
object ProposalSubmissionBar: | ||
private type Props = ProposalSubmissionBar | ||
|
||
private object IsUpdatingStatus extends NewType[Boolean] | ||
private type IsUpdatingStatus = IsUpdatingStatus.Type | ||
|
||
private def doUpdateStatus( | ||
programId: Program.Id, | ||
isUpdatingStatus: View[IsUpdatingStatus], | ||
setLocalProposalStatus: ProposalStatus => IO[Unit], | ||
setErrorMessage: Option[String] => IO[Unit] | ||
)( | ||
newStatus: ProposalStatus | ||
)(using | ||
FetchClient[IO, ObservationDB], | ||
Logger[IO] | ||
): Callback = | ||
(for { | ||
_ <- SetProposalStatus[IO] | ||
.execute: | ||
SetProposalStatusInput(programId = programId.assign, status = newStatus) | ||
.onError: | ||
case ResponseException(errors, _) => | ||
setErrorMessage(errors.head.message.some) | ||
case e => | ||
setErrorMessage(Some(e.getMessage.toString)) | ||
.void | ||
_ <- setLocalProposalStatus(newStatus) | ||
} yield ()).switching(isUpdatingStatus.async, IsUpdatingStatus(_)).runAsync | ||
|
||
private val component = | ||
ScalaFnComponent | ||
.withHooks[Props] | ||
.useContext(AppContext.ctx) | ||
.useStateView(IsUpdatingStatus(false)) | ||
.useStateView(none[String]) // Submission error message | ||
.useLayoutEffectWithDepsBy((props, _, _, _) => props.callId): (_, _, _, e) => | ||
_ => e.set(none) // Reset error message on CfP change | ||
.useStreamOnMount: | ||
Stream | ||
.fixedRateStartImmediately[IO](1.second) | ||
.evalMap: _ => | ||
IO.monotonic.map(finiteDuration => Timestamp.ofEpochMilli(finiteDuration.toMillis)) | ||
.render: (props, ctx, isUpdatingStatus, errorMessage, nowPot) => | ||
import ctx.given | ||
|
||
val updateStatus: ProposalStatus => Callback = | ||
doUpdateStatus( | ||
props.programId, | ||
isUpdatingStatus, | ||
props.proposalStatus.async.set, | ||
errorMessage.async.set | ||
) | ||
|
||
nowPot.toOption.flatten.map: now => | ||
val isDueDeadline: Boolean = props.deadline.exists(_ < now) | ||
|
||
Toolbar(left = | ||
<.div(ExploreStyles.ProposalSubmissionBar)( | ||
Tag( | ||
value = props.proposalStatus.get.name, | ||
severity = | ||
if (props.proposalStatus.get === ProposalStatus.Accepted) Tag.Severity.Success | ||
else Tag.Severity.Danger | ||
) | ||
.when(props.proposalStatus.get > ProposalStatus.Submitted), | ||
// TODO: Validate proposal before allowing submission | ||
React | ||
.Fragment( | ||
Button( | ||
label = "Submit Proposal", | ||
onClick = updateStatus(ProposalStatus.Submitted), | ||
disabled = isUpdatingStatus.get.value || props.callId.isEmpty || isDueDeadline | ||
).compact.tiny, | ||
props.deadline.map: deadline => | ||
val (deadlineStr, left): (String, Option[String]) = | ||
Proposal.deadlineAndTimeLeft(now, deadline) | ||
val text: String = | ||
left.fold(deadlineStr)(l => s"$deadlineStr [$l]") | ||
val severity: Message.Severity = | ||
left.fold(Message.Severity.Error)(_ => Message.Severity.Info) | ||
|
||
<.span(ExploreStyles.ProposalDeadline)( | ||
Message( | ||
text = s"Deadline: $text", | ||
severity = severity | ||
) | ||
) | ||
) | ||
.when: | ||
props.isStdUser && props.proposalStatus.get === ProposalStatus.NotSubmitted | ||
, | ||
Button( | ||
"Retract Proposal", | ||
severity = Button.Severity.Warning, | ||
onClick = updateStatus(ProposalStatus.NotSubmitted), | ||
disabled = isUpdatingStatus.get.value || isDueDeadline | ||
).compact.tiny | ||
.when: | ||
props.isStdUser && props.proposalStatus.get === ProposalStatus.Submitted && !isDueDeadline | ||
, | ||
errorMessage.get | ||
.map(r => Message(text = r, severity = Message.Severity.Error)) | ||
) | ||
) |
Oops, something went wrong.