Skip to content

Commit

Permalink
Merge pull request #4170 from gemini-hlsw/sc-3153-enforce-proposal-de…
Browse files Browse the repository at this point in the history
…adlines

Enforce proposal deadlines
  • Loading branch information
rpiaggio authored Sep 25, 2024
2 parents e075ca8 + e4bc500 commit 12dd435
Show file tree
Hide file tree
Showing 6 changed files with 229 additions and 134 deletions.
13 changes: 1 addition & 12 deletions explore/src/main/scala/explore/ExploreLayout.scala
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ import explore.shortcuts.*
import explore.shortcuts.given
import explore.utils.*
import japgolly.scalajs.react.*
import japgolly.scalajs.react.React
import japgolly.scalajs.react.extra.router.ResolutionWithProps
import japgolly.scalajs.react.extra.router.SetRouteVia
import japgolly.scalajs.react.vdom.html_<^.*
Expand All @@ -35,7 +34,6 @@ import lucuma.react.common.*
import lucuma.react.hotkeys.*
import lucuma.react.hotkeys.hooks.*
import lucuma.react.primereact.ConfirmDialog
import lucuma.react.primereact.Message
import lucuma.react.primereact.Sidebar
import lucuma.react.primereact.Toast
import lucuma.react.primereact.ToastRef
Expand Down Expand Up @@ -213,11 +211,6 @@ object ExploreLayout:
p.deadline(c, piP)
.flatten

val deadlineStr: String =
deadline
.map(Proposal.deadlineString)
.orEmpty

val cacheKey: String =
userVault.get
.map(_.user)
Expand Down Expand Up @@ -298,11 +291,7 @@ object ExploreLayout:
else
<.div(LayoutStyles.MainBody, LayoutStyles.WithMessage.when(isSubmitted))(
props.resolution.renderP(props.model),
if (isSubmitted)
Message(text =
s"The proposal has been submitted as ${proposalReference.foldMap(_.label)} and may be retracted until the proposal deadline at ${deadlineStr}."
)
else EmptyVdom
TagMod.when(isSubmitted)(SubmittedProposalMessage(proposalReference, deadline))
)
)
)
Expand Down
51 changes: 51 additions & 0 deletions explore/src/main/scala/explore/SubmittedProposalMessage.scala
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 explore/src/main/scala/explore/proposal/CallDeadline.scala

This file was deleted.

153 changes: 153 additions & 0 deletions explore/src/main/scala/explore/proposal/ProposalSubmissionBar.scala
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))
)
)
Loading

0 comments on commit 12dd435

Please sign in to comment.