Skip to content
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

Fix missing Program tab #4551

Merged
merged 3 commits into from
Feb 10, 2025
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// 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 queries.common

import clue.GraphQLSubquery
import clue.annotation.GraphQL
import explore.model.CallForProposal
import lucuma.schemas.ObservationDB

@GraphQL
object CallForProposalsSubquery
extends GraphQLSubquery.Typed[ObservationDB, CallForProposal]("CallForProposal"):
override val subquery: String = s"""
{
id
semester
title
cfpType: type
nonPartnerDeadline
active {
start
end
}
partners {
partner
submissionDeadline
}
coordinateLimits {
north $SiteCoordinatesLimitsSubquery
south $SiteCoordinatesLimitsSubquery
}
}
"""
20 changes: 1 addition & 19 deletions explore/src/clue/scala/queries/common/CallsQueriesGQL.scala
Original file line number Diff line number Diff line change
Expand Up @@ -13,25 +13,7 @@ object CallsQueriesGQL:
val document: String = s"""
query {
callsForProposals(WHERE: {isOpen: {EQ: true}}) {
matches {
id
semester
title
cfpType: type
nonPartnerDeadline
active {
start
end
}
partners {
partner
submissionDeadline
}
coordinateLimits {
north $SiteCoordinatesLimitsSubquery
south $SiteCoordinatesLimitsSubquery
}
}
matches $CallForProposalsSubquery
}
}
"""
Expand Down
4 changes: 1 addition & 3 deletions explore/src/clue/scala/queries/common/ProposalSubquery.scala
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,7 @@ import lucuma.schemas.ObservationDB
object ProposalSubquery extends GraphQLSubquery.Typed[ObservationDB, Proposal]("Proposal"):
override val subquery: String = s"""
{
call {
id
}
call $CallForProposalsSubquery
category
reference {
label
Expand Down
12 changes: 6 additions & 6 deletions explore/src/main/scala/explore/ExploreLayout.scala
Original file line number Diff line number Diff line change
Expand Up @@ -222,12 +222,12 @@ object ExploreLayout:
val deadline: Option[Timestamp] =
programSummaries.toOption
.flatMap: programSummaries =>
(ProgramSummaries.proposal.getOption(programSummaries).flatten,
RootModel.cfps.get(view.get).toOption
).mapN: (p, c) =>
val piP = ProgramSummaries.piPartner.getOption(programSummaries)
p.deadline(c, piP)
.flatten
ProgramSummaries.proposal
.getOption(programSummaries)
.flatten
.flatMap: p =>
val piP = ProgramSummaries.piPartner.getOption(programSummaries)
p.deadline(piP)

val cacheKey: String =
userVault.get
Expand Down
4 changes: 0 additions & 4 deletions explore/src/main/scala/explore/Routing.scala
Original file line number Diff line number Diff line change
Expand Up @@ -194,9 +194,6 @@ object Routing:
programDetails <-
programSummaries.model.zoom(ProgramSummaries.optProgramDetails).toOptionView
proposal <- programDetails.get.proposal
callId <- proposal.callId
cfps <- model.rootModel.get.cfps.toOption
cfp <- cfps.find(_.id === callId)
yield ProgramTabContents(
routingInfo.programId,
programDetails,
Expand All @@ -207,7 +204,6 @@ object Routing:
programSummaries.get.targets,
model.rootModel.zoom(RootModel.vault).get,
programSummaries.get.programTimesPot,
cfp.semester,
userPreferences(model.rootModel),
model.userIsReadonlyCoi
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -284,7 +284,7 @@ object ProgramCacheController
ProgramQueriesGQL.ProgramEditDetailsSubscription.Data,
ProgramSummaries => ProgramSummaries
] =
_.map(_.programEdit.value.proposal.flatMap(_.callId)).changes.void
_.map(_.programEdit.value.proposal.flatMap(_.call.map(_.id))).changes.void
.evalMap(_ => updateObservationsWorkflows(props.programId.toWhereObservation))

val updateProgramDetails: Resource[IO, Stream[IO, ProgramSummaries => ProgramSummaries]] =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ trait ProposalQueries:
extension (proposal: Proposal)
def toInput: ProposalPropertiesInput =
ProposalPropertiesInput(
callId = proposal.callId.orUnassign,
callId = proposal.call.map(_.id).orUnassign,
category = proposal.category.orUnassign,
`type` = proposal.proposalType.map(_.toInput).orUnassign
)
Expand Down
23 changes: 15 additions & 8 deletions explore/src/main/scala/explore/programs/ProgramDetailsTile.scala
Original file line number Diff line number Diff line change
Expand Up @@ -18,19 +18,20 @@ import explore.model.ProgramUser
import japgolly.scalajs.react.*
import japgolly.scalajs.react.vdom.html_<^.*
import lucuma.core.model.Program
import lucuma.core.model.Semester
import lucuma.core.syntax.display.*
import lucuma.core.util.DateInterval
import lucuma.react.common.ReactFnProps
import lucuma.refined.*
import lucuma.ui.primereact.CheckboxView
import lucuma.ui.primereact.FormInfo
import lucuma.ui.primereact.given

import java.time.LocalDate

case class ProgramDetailsTile(
programId: Program.Id,
programDetails: View[ProgramDetails],
programTimes: Pot[ProgramTimes],
semester: Semester,
userIsReadonlyCoi: Boolean
) extends ReactFnProps(ProgramDetailsTile.component)

Expand All @@ -41,19 +42,25 @@ object ProgramDetailsTile:
useContext(AppContext.ctx).map: ctx =>
import ctx.given

val details: ProgramDetails = props.programDetails.get
val thesis: Boolean = details.allUsers.exists(_.thesis.exists(_ === true))
val users: View[List[ProgramUser]] = props.programDetails.zoom(ProgramDetails.allUsers)
val newDataNotificationView =
val details: ProgramDetails = props.programDetails.get
val thesis: Boolean = details.allUsers.exists(_.thesis.exists(_ === true))
val users: View[List[ProgramUser]] = props.programDetails.zoom(ProgramDetails.allUsers)
val newDataNotificationView =
props.programDetails
.zoom(ProgramDetails.shouldNotify)
.withOnMod(b => ProgramQueries.updateGoaShouldNotify[IO](props.programId, b).runAsync)
val cfpActivePeriod: Option[DateInterval] = details.proposal.flatMap(_.call).map(_.active)

// We `should` always have a call for proposal if we get here, but...
def dateOrMissing(o: Option[LocalDate], label: String) =
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we can just omit the element if the date is missing?

Also, could we add explicit types?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could do that, but I thought it might be easier to debug in the future if we ever run into this again. It should never happen since the program tab is only for ACCEPTED proposals, which should have a CfP. But, omitting them would be cleaner, I guess.

val s = o.fold("Missing!")(Constants.GppDateFormatter.format)
FormInfo(s, label)

<.div(ExploreStyles.ProgramDetailsTile)(
<.div(ExploreStyles.ProgramDetailsInfoArea, ExploreStyles.ProgramDetailsLeft)(
FormInfo(details.reference.map(_.label).getOrElse("---"), "Reference"),
FormInfo(Constants.GppDateFormatter.format(props.semester.start.localDate), "Start"),
FormInfo(Constants.GppDateFormatter.format(props.semester.end.localDate), "End"),
dateOrMissing(cfpActivePeriod.map(_.start), "Start"),
dateOrMissing(cfpActivePeriod.map(_.end), "End"),
// Thesis should be set True if any of the investigators will use the proposal as part of their thesis (3390)
FormInfo(if (thesis) "Yes" else "No", "Thesis"),
FormInfo(s"${details.proprietaryMonths} months", "Proprietary")
Expand Down
142 changes: 54 additions & 88 deletions explore/src/main/scala/explore/proposal/ProposalDetailsTile.scala
Original file line number Diff line number Diff line change
Expand Up @@ -157,13 +157,12 @@ object ProposalDetailsBody:
private def renderFn(
props: Props,
totalHours: View[Hours],
// minPct2: View[IntPercent],
showDialog: View[Visible],
splitsList: View[List[PartnerSplit]]
)(using Logger[IO]): VdomNode = {
val proposalCfpView: View[Proposal] =
props.proposalAligner.viewMod { p =>
ProposalPropertiesInput.callId.replace(p.callId.orUnassign) >>>
ProposalPropertiesInput.callId.replace(p.call.map(_.id).orUnassign) >>>
ProposalPropertiesInput.`type`.replace(p.proposalType.map(_.toInput).orUnassign)
}

Expand All @@ -172,13 +171,20 @@ object ProposalDetailsBody:

val titleView = titleAligner.view(_.orUnassign)

val callId: Option[CallForProposals.Id] = props.proposalAligner.get.callId
val scienceSubtype = props.proposalAligner.get.proposalType.map(_.scienceSubtype)
val scienceSubtype = props.proposalAligner.get.proposalType.map(_.scienceSubtype)

val selectedCfp = callId.flatMap(id => props.cfps.find(_.id === id))
val isCfpSelected = selectedCfp.isDefined
val subtypes = selectedCfp.map(_.cfpType.subTypes)
val hasSubtypes = subtypes.exists(_.size > 1)
val selectedCfp: Option[CallForProposal] = props.proposalAligner.get.call
val isCfpSelected = selectedCfp.isDefined
val subtypes = selectedCfp.map(_.cfpType.subTypes)
val hasSubtypes = subtypes.exists(_.size > 1)

// need to include the current cfp as disabled if it doesn't exist in the list of calls
val cfpOptions: List[SelectItem[CallForProposal]] =
selectedCfp
.filterNot(cfp => props.cfps.exists(_.id === cfp.id))
.map(cfp => SelectItem(cfp, cfp.title, disabled = true))
.toList ++
props.cfps.map(cfp => SelectItem(cfp, cfp.title))

val categoryAligner: Aligner[Option[TacCategory], Input[TacCategory]] =
props.proposalAligner.zoom(Proposal.category, ProposalPropertiesInput.category.modify)
Expand Down Expand Up @@ -406,17 +412,16 @@ object ProposalDetailsBody:
FormDropdownOptional(
id = "cfp".refined,
label = React.Fragment("Call For Proposal", HelpIcon("proposal/main/cfp.md".refined)),
value = callId,
options = props.cfps.map(r => SelectItem(r.id, r.title)),
onChange = _.map { cid =>
val call = props.cfps.find(_.id === cid)
value = selectedCfp,
options = cfpOptions,
onChange = _.map { cfp =>
proposalCfpView.mod(
_.copy(callId = cid.some, proposalType = call.map(_.cfpType.defaultType))
_.copy(call = cfp.some, proposalType = cfp.cfpType.defaultType.some)
)
}.orEmpty,
disabled = props.readonly,
modifiers = List(^.id := "cfp"),
clazz = ExploreStyles.WarningInput.when_(callId.isEmpty)
clazz = ExploreStyles.WarningInput.when_(selectedCfp.isEmpty)
),
// Proposal type selector, visible when cfp is selected and has more than one subtpye
FormDropdown(
Expand Down Expand Up @@ -446,80 +451,41 @@ object ProposalDetailsBody:
)
}

private val component =
ScalaFnComponent
.withHooks[Props]
.useContext(AppContext.ctx)
// total time - we need `Hours` for editing and also to preserve if
// the user switches between classes with and without total time.
.useStateViewBy: (props, _) =>
props.proposal.proposalType
.flatMap(ProposalType.totalTime.getOption)
.map(toHours)
.getOrElse(Hours.unsafeFrom(0))
// .useStateViewBy((props, _, _, _) =>
// // mininum percent total time = need to preserve between class switches
// props.proposal
// .zoom(Proposal.proposalClass.andThen(ProposalClass.minPercentTotalTime))
// .get
// .getOrElse(IntPercent.unsafeFrom(80))
// )
.useStateView(Visible.Hidden) // show partner splits modal
.useStateView(List.empty[PartnerSplit]) // partner splits modal
// Update the partner splits when a new callId is set
.useEffectWithDepsBy((props, _, _, _, _) => (props.proposal.callId, props.cfps)):
(props, _, _, _, ps) =>
(callId, cfps) =>
callId.foldMap(cid =>
val currentSplits = Proposal.proposalType.some
.andThen(ProposalType.partnerSplits)
.getOption(props.proposal)
val cfpPartners = cfps
.find(_.id === cid)
.foldMap(_.partners.map(_.partner))
val proposalPartners = currentSplits.orEmpty.filter(_._2 > 0).map(_.partner)

if (proposalPartners.nonEmpty && proposalPartners.forall(cfpPartners.contains))
ps.set(currentSplits.orEmpty)
else
ps.set(cfpPartners.map(p => PartnerSplit(p, 0.refined)))
)
// .useEffectWithDepsBy((props, _, _, _, _, _, _, _, _) => props.proposal.get.proposalClass)(
// // Deal with changes to the ProposalClass.
// (props, _, _, totalHours, minPct2, classType, _, _, oldClass) =>
// newClass => {
// val setClass =
// if (oldClass.get === newClass) Callback.empty
// else props.proposal.zoom(Proposal.proposalClass).set(newClass)
// val newType = ProposalClassType.fromProposalClass(newClass)
// val setType = if (classType.get === newType) Callback.empty else classType.set(newType)
// val newHours = ProposalClass.totalTime.getOption(newClass).map(toHours)
// val setHours = newHours
// .flatMap(h => if (h === totalHours.get) none else h.some)
// .foldMap(totalHours.set)
// val newPct2 = ProposalClass.minPercentTotalTime.getOption(newClass)
// val setPct2 =
// newPct2.flatMap(p => if (p === minPct2.get) none else p.some).foldMap(minPct2.set)
// setClass >> setType >> setHours >> setPct2
// }
// )
.render:
(
props,
ctx,
totalHours,
// minPct2,
showDialog,
splitsList
) =>
renderFn(
props,
totalHours,
// minPct2,
showDialog,
splitsList
)(using ctx.logger)

private val component = ScalaFnComponent[Props](props =>
for {
ctx <- useContext(AppContext.ctx)
totalHours <- useStateView:
// we need `Hours` for editing
props.proposal.proposalType
.flatMap(ProposalType.totalTime.getOption)
.map(toHours)
.getOrElse(Hours.unsafeFrom(0))
showDialog <- useStateView(Visible.Hidden) // show partner splits modal
splitsList <- useStateView(List.empty[PartnerSplit]) // partner splits modal
_ <- useEffectWithDeps((props.proposal.call.map(_.id), props.cfps)):
// Update the partner splits when a new callId is set
(callId, cfps) =>
callId.foldMap(cid =>
val currentSplits = Proposal.proposalType.some
.andThen(ProposalType.partnerSplits)
.getOption(props.proposal)
val cfpPartners = cfps
.find(_.id === cid)
.foldMap(_.partners.map(_.partner))
val proposalPartners = currentSplits.orEmpty.filter(_._2 > 0).map(_.partner)

if (proposalPartners.nonEmpty && proposalPartners.forall(cfpPartners.contains))
splitsList.set(currentSplits.orEmpty)
else
splitsList.set(cfpPartners.map(p => PartnerSplit(p, 0.refined)))
)
} yield renderFn(
props,
totalHours,
showDialog,
splitsList
)(using ctx.logger)
)
case class ProposalDetailsTitle(undoer: Undoer, tileSize: TileSizeState, readonly: Boolean)
extends ReactFnProps(ProposalDetailsTitle.component)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,10 @@ object ProposalSubmissionBar:
props.canSubmit && props.proposalStatus.get === ProposalStatus.Submitted && !isDueDeadline
,
errorMessage.get
.map(r => Message(text = r, severity = Message.Severity.Error))
.map(r =>
<.span(ExploreStyles.ProposalDeadline)(
Message(text = r, severity = Message.Severity.Error)
)
)
)
)
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ object ProposalTabContents:
props.programDetails.zoom(ProgramDetails.piPartner.some).get

val deadline: Option[Timestamp] =
proposal.get.deadline(props.cfps, piPartner)
proposal.get.deadline(piPartner)

<.div(ExploreStyles.ProposalTab)(
ProposalEditor(
Expand All @@ -133,7 +133,7 @@ object ProposalTabContents:
props.programId,
props.programDetails.zoom(ProgramDetails.proposalStatus),
deadline,
proposal.get.callId,
proposal.get.call.map(_.id),
isStdUser && !props.userIsReadonlyCoi,
props.hasUndefinedObservations
)
Expand Down
Loading
Loading