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

Edit support users in program tab #4156

Merged
merged 11 commits into from
Sep 19, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
Expand Up @@ -491,7 +491,9 @@ object ExploreStyles:
// Program Tab
val ProgramDetailsTile: Css = Css("program-details-tile")
val ProgramDetailsInfoArea: Css = Css("program-details-info-area")
val ProgramDetailsLeft: Css = Css("program-details-left")
val ProgramTabTable: Css = Css("program-tab-table")
val ProgramDetailsUsers: Css = Css("program-details-users")

val FocusedInfo: Css = Css("explore-focused-info")

Expand Down
2 changes: 1 addition & 1 deletion common/src/main/scala/explore/model/reusability.scala
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ object reusability:
given [D: Eq]: Reusability[Atom[D]] = Reusability.byEq
given Reusability[ExecutionVisits] = Reusability.byEq
given Reusability[ProgramUserWithRole] = Reusability.byEq
given Reusability[CoIInvitation] = Reusability.byEq
given Reusability[UserInvitation] = Reusability.byEq
given Reusability[IsActive] = Reusability.byEq
given Reusability[PAProperties] = Reusability.byEq
given Reusability[GraphResult] = Reusability.byEq
Expand Down
21 changes: 17 additions & 4 deletions common/src/main/webapp/sass/explore.scss
Original file line number Diff line number Diff line change
Expand Up @@ -3245,17 +3245,30 @@ a.explore-upgrade-link {
justify-content: space-between;

.program-details-info-area {
display: grid;
grid-template-columns: [label] auto [value] 1fr;
gap: 0.25em 1em;
grid-auto-rows: max-content;
flex: 1 1 0;
padding: 1em;

&.program-details-left {
display: grid;
grid-template-columns: [label] auto [value] 1fr;
gap: 0.25em 1em;
grid-auto-rows: max-content;
}

label {
text-align: right;
font-weight: bold;
}

.program-details-users {
margin-bottom: 1em;

label {
display: flex;
gap: 1em;
align-items: center;
}
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,24 +6,25 @@ package queries.common
import clue.GraphQLOperation
import clue.annotation.GraphQL
import lucuma.schemas.ObservationDB
import explore.model.CoIInvitation
import explore.model.UserInvitation
import explore.model.ProgramInvitation

object InvitationQueriesGQL:
@GraphQL
trait CreateInviteMutation extends GraphQLOperation[ObservationDB]:
val document: String = s"""
mutation($$programId: ProgramId!, $$recipientEmail: String!) {
mutation($$programId: ProgramId!, $$recipientEmail: String!, $$role: ProgramUserRole!) {
createUserInvitation(input: {
programId: $$programId,
recipientEmail: $$recipientEmail,
role: COI
role: $$role
}) {
key
invitation {
id
status
recipientEmail
role
email {
status
}
Expand All @@ -34,7 +35,7 @@ object InvitationQueriesGQL:

object Data:
object CreateUserInvitation:
type Invitation = CoIInvitation
type Invitation = UserInvitation

@GraphQL
trait RevokeInvitationMutation extends GraphQLOperation[ObservationDB]:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,17 @@ package queries.common

import clue.GraphQLSubquery
import clue.annotation.GraphQL
import explore.model.CoIInvitation
import explore.model.UserInvitation
import lucuma.schemas.ObservationDB

@GraphQL
object ProgramInvitationsSubquery
extends GraphQLSubquery.Typed[ObservationDB, CoIInvitation]("CoIInvitation"):
extends GraphQLSubquery.Typed[ObservationDB, UserInvitation]("CoIInvitation"):
override val subquery: String = """
{
id
recipientEmail
role
status
email {
status
Expand Down
11 changes: 7 additions & 4 deletions explore/src/main/scala/explore/Routing.scala
Original file line number Diff line number Diff line change
Expand Up @@ -157,15 +157,18 @@ object Routing:
)
)

private def programTab(model: RootModelViews): VdomElement =
private def programTab(page: Page, model: RootModelViews): VdomElement =
withProgramSummaries(model): programSummaries =>
val routingInfo = RoutingInfo.from(page)
for
programDetails <- programSummaries.model.get.optProgramDetails
proposal <- programDetails.proposal
programDetails <-
programSummaries.model.zoom(ProgramSummaries.optProgramDetails).toOptionView
proposal <- programDetails.get.proposal
callId <- proposal.callId
cfps <- model.rootModel.get.cfps
cfp <- cfps.find(_.id === callId)
yield ProgramTabContents(
routingInfo.programId,
programDetails,
model.rootModel.zoom(RootModel.vault).get,
programSummaries.get.programTimesPot,
Expand Down Expand Up @@ -208,7 +211,7 @@ object Routing:

| dynamicRouteCT(
(root / id[Program.Id] / "program").xmapL(ProgramPage.iso)
) ~> dynRenderP { case (_, m) => programTab(m) }
) ~> dynRenderP { case (p, m) => programTab(p, m) }

| dynamicRouteCT(
(root / id[Program.Id] / "proposal").xmapL(ProposalPage.iso)
Expand Down
58 changes: 39 additions & 19 deletions explore/src/main/scala/explore/programs/ProgramDetailsTile.scala
Original file line number Diff line number Diff line change
Expand Up @@ -5,36 +5,45 @@ package explore.programs

import cats.syntax.all.*
import crystal.Pot
import crystal.react.View
import explore.components.ui.ExploreStyles
import explore.model.Constants
import explore.model.ProgramDetails
import explore.model.ProgramTimes
import explore.model.ProgramUserWithRole
import explore.model.enums.ProgramUserRole
import explore.model.UserInvitation
import japgolly.scalajs.react.*
import japgolly.scalajs.react.vdom.html_<^.*
import lucuma.core.model.Access
import lucuma.core.model.Program
import lucuma.core.model.Semester
import lucuma.core.syntax.display.*
import lucuma.react.common.ReactFnProps
import lucuma.ui.primereact.FormInfo

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

object ProgramDetailsTile:
private type Props = ProgramDetailsTile

val component = ScalaFnComponent[Props]: props =>
val details: ProgramDetails = props.programDetails
val thesis: Boolean = details.allUsers.exists(_.thesis.exists(_ === true))
val support: List[ProgramUserWithRole] =
details.allUsers.filter(_.role.contains_(ProgramUserRole.Support))
private val EditSupportAccesses: Set[Access] = Set(Access.Admin, Access.Staff)

private val component = ScalaFnComponent[Props]: props =>
val details: ProgramDetails = props.programDetails.get
val thesis: Boolean = details.allUsers.exists(_.thesis.exists(_ === true))
val users: View[List[ProgramUserWithRole]] = props.programDetails.zoom(ProgramDetails.allUsers)
val invitations: View[List[UserInvitation]] =
props.programDetails.zoom(ProgramDetails.invitations)
val readonly: Boolean = !EditSupportAccesses.contains_(props.currentUserAccess)

<.div(ExploreStyles.ProgramDetailsTile)(
<.div(ExploreStyles.ProgramDetailsInfoArea)(
<.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"),
Expand All @@ -48,14 +57,25 @@ object ProgramDetailsTile:
TimeAccountingTable(props.programTimes)
),
<.div(ExploreStyles.ProgramDetailsInfoArea)(
// The Contact scientists are the program SUPPORT role which has been requested to be split into two (3278): "Principal Support" and "Additional Support".
FormInfo(support.map(_.nameWithEmail).mkString(", "), "Contact Scientists")
.when(support.nonEmpty)
// FormInfo("", "Principal Support"),
// FormInfo("", "Additional Support"),
// The two Notifications flags are user-settable and determine whether the archive sends email notifications for new data and whether the ODB sends notifications for expired timing windows (3388, 3389)
// FormInfo("", "Notifications")
// The Eavesdropping` UI will allow PIs of accepted programs to select dates when they are available for eavesdropping. This is not needed for XT. (NEED TICKET)
// FormInfo("", "Eavesdropping")
SupportUsers(
props.programId,
users,
invitations,
"Principal Support",
SupportUsers.SupportRole.Primary,
readonly
),
SupportUsers(
props.programId,
users,
invitations,
"Additional Support",
SupportUsers.SupportRole.Secondary,
readonly
)
// The two Notifications flags are user-settable and determine whether the archive sends email notifications for new data and whether the ODB sends notifications for expired timing windows (3388, 3389)
// FormInfo("", "Notifications")
// The Eavesdropping` UI will allow PIs of accepted programs to select dates when they are available for eavesdropping. This is not needed for XT. (NEED TICKET)
// FormInfo("", "Eavesdropping")
)
)
57 changes: 57 additions & 0 deletions explore/src/main/scala/explore/programs/SupportUsers.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// 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.programs

import cats.data.NonEmptySet
import crystal.react.View
import explore.components.ui.ExploreStyles
import explore.model.ProgramUserWithRole
import explore.model.UserInvitation
import explore.users.InviteUserButton
import explore.users.ProgramUsersTable
import japgolly.scalajs.react.*
import japgolly.scalajs.react.vdom.html_<^.*
import lucuma.core.enums.ProgramUserRole
import lucuma.core.model.Program
import lucuma.react.common.ReactFnProps

case class SupportUsers(
programId: Program.Id,
users: View[List[ProgramUserWithRole]],
invitations: View[List[UserInvitation]],
title: String,
supportRole: SupportUsers.SupportRole,
readonly: Boolean
) extends ReactFnProps(SupportUsers.component)

object SupportUsers:
private type Props = SupportUsers

enum SupportRole(protected[SupportUsers] val role: ProgramUserRole):
case Primary extends SupportRole(ProgramUserRole.SupportPrimary)
case Secondary extends SupportRole(ProgramUserRole.SupportSecondary)

private val component = ScalaFnComponent[Props]: props =>
<.div(ExploreStyles.ProgramDetailsUsers)(
<.label(
props.title,
InviteUserButton(props.programId, props.supportRole.role, props.invitations)
.unless(props.readonly)
),
ProgramUsersTable(
props.programId,
props.users,
props.invitations,
NonEmptySet.one(props.supportRole.role),
props.readonly,
hiddenColumns = Set(
ProgramUsersTable.Column.Partner,
ProgramUsersTable.Column.EducationalStatus,
ProgramUsersTable.Column.Thesis,
ProgramUsersTable.Column.Gender,
ProgramUsersTable.Column.Role,
ProgramUsersTable.Column.OrcidId
)
)
)
43 changes: 43 additions & 0 deletions explore/src/main/scala/explore/proposal/CallDeadline.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// 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 crystal.react.hooks.*
import explore.components.ui.ExploreStyles
import explore.model.Proposal
import fs2.Stream
import japgolly.scalajs.react.*
import japgolly.scalajs.react.vdom.html_<^.*
import lucuma.core.util.Timestamp
import lucuma.react.common.ReactFnProps
import lucuma.react.primereact.Message

import java.time.Instant
import scala.concurrent.duration.*

case class CallDeadline(deadline: Timestamp) extends ReactFnProps(CallDeadline.component)

object CallDeadline:
private type Props = CallDeadline

private val component =
ScalaFnComponent
.withHooks[Props]
.useStreamOnMount:
Stream.eval(IO(Instant.now())) ++
Stream
.awakeDelay[IO](1.seconds)
.flatMap(_ => Stream.eval(IO(Instant.now())))
.render: (p, n) =>
n.toOption.map: n =>
val (deadlineStr, left) = Proposal.deadlineAndTimeLeft(n, p.deadline)
val text = left.fold(deadlineStr)(l => s"$deadlineStr [$l]")

<.span(ExploreStyles.ProposalDeadline)(
Message(
text = s"Deadline: $text",
severity = Message.Severity.Info
)
)
Loading
Loading