From 0deb5e35275b19f26c9261bacd702e9db04b3ede Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ra=C3=BAl=20Piaggio?= Date: Mon, 16 Sep 2024 10:40:41 -0300 Subject: [PATCH 01/11] refactor --- .../scala/explore/proposal/ProgramUsers.scala | 3 +- .../explore/proposal/ProposalEditor.scala | 156 +++++++++--------- 2 files changed, 75 insertions(+), 84 deletions(-) diff --git a/explore/src/main/scala/explore/proposal/ProgramUsers.scala b/explore/src/main/scala/explore/proposal/ProgramUsers.scala index 552363d3d6..f2f5cc0aea 100644 --- a/explore/src/main/scala/explore/proposal/ProgramUsers.scala +++ b/explore/src/main/scala/explore/proposal/ProgramUsers.scala @@ -24,7 +24,6 @@ import lucuma.react.primereact.Button import lucuma.react.primereact.OverlayPanelRef import lucuma.ui.primereact.* import lucuma.ui.syntax.all.given -import org.typelevel.log4cats.Logger enum CreateInviteProcess(private val tag: String) derives Enumerated: case Idle extends CreateInviteProcess("idle") @@ -62,7 +61,7 @@ object ProgramUsers: users: View[List[ProgramUserWithRole]], invitations: View[List[CoIInvitation]], ref: OverlayPanelRef - )(using AppContext[IO], Logger[IO]) = + ) = Tile( ProposalTabTileIds.UsersId.id, "Investigators", diff --git a/explore/src/main/scala/explore/proposal/ProposalEditor.scala b/explore/src/main/scala/explore/proposal/ProposalEditor.scala index f76040b474..2bf0f3a19f 100644 --- a/explore/src/main/scala/explore/proposal/ProposalEditor.scala +++ b/explore/src/main/scala/explore/proposal/ProposalEditor.scala @@ -31,9 +31,7 @@ import japgolly.scalajs.react.vdom.html_<^.* import lucuma.core.model.Program import lucuma.core.model.User import lucuma.react.common.ReactFnProps -import lucuma.react.primereact.OverlayPanelRef import lucuma.react.primereact.hooks.UseOverlayPanelRef.implicits.* -import lucuma.react.resizeDetector.UseResizeDetectorReturn import lucuma.react.resizeDetector.hooks.* import lucuma.refined.* import lucuma.schemas.ObservationDB @@ -63,85 +61,6 @@ case class ProposalEditor( object ProposalEditor: private type Props = ProposalEditor - private def renderFn( - props: Props, - resize: UseResizeDetectorReturn, - ref: OverlayPanelRef - )(using ctx: AppContext[IO]) = { - import ctx.given - - val undoCtx: UndoContext[Proposal] = UndoContext(props.undoStacks, props.proposal) - - val aligner: Aligner[Proposal, ProposalPropertiesInput] = - Aligner( - undoCtx, - UpdateProposalInput( - programId = props.programId.assign, - SET = ProposalPropertiesInput() - ), - (ProposalQueriesGQL.UpdateProposalMutation[IO].execute(_)).andThen(_.void) - ).zoom(Iso.id[Proposal].asLens, UpdateProposalInput.SET.modify) - - val abstractAligner: Aligner[Option[NonEmptyString], Input[NonEmptyString]] = - aligner.zoom(Proposal.abstrakt, ProposalPropertiesInput.`abstract`.modify) - - val abstractView = abstractAligner.view(_.orUnassign) - - val defaultLayouts = ExploreGridLayouts.sectionLayout(GridLayoutSection.ProposalLayout) - - val detailsTile = - Tile(ProposalTabTileIds.DetailsId.id, "Details")( - _ => - ProposalDetailsBody(props.proposal, - aligner, - props.timeEstimateRange, - props.cfps, - props.readonly - ), - (_, s) => ProposalDetailsTitle(undoCtx, s) - ) - - val usersTile = - ProgramUsers.programUsersTile(props.programId, - props.readonly, - props.users, - props.invitations, - ref - ) - - val abstractTile = - Tile(ProposalTabTileIds.AbstractId.id, - "Abstract", - bodyClass = ExploreStyles.ProposalAbstract - )(_ => - FormInputTextAreaView( - id = "abstract".refined, - value = abstractView.as(OptionNonEmptyStringIso) - )(^.disabled := props.readonly) - ) - - val attachmentsTile = - Tile(ProposalTabTileIds.AttachmentsId.id, "Attachments")(_ => - props.authToken.map(token => - ProposalAttachmentsTable(props.programId, token, props.attachments, props.readonly) - ) - ) - - <.div( - ExploreStyles.MultiPanelTile, - InviteUserPopup(props.programId, props.invitations, ref), - TileController( - props.optUserId, - resize.width.getOrElse(1), - defaultLayouts, - props.layout, - List(detailsTile, usersTile, abstractTile, attachmentsTile), - GridLayoutSection.ProposalLayout, - storeLayout = true - ) - ).withRef(resize.ref) - } - private val component = ScalaFnComponent .withHooks[Props] @@ -175,4 +94,77 @@ object ProposalEditor: .useResizeDetector() .useOverlayPanelRef .render: (props, ctx, resize, overlayRef) => - renderFn(props, resize, overlayRef)(using ctx) + import ctx.given + + val undoCtx: UndoContext[Proposal] = UndoContext(props.undoStacks, props.proposal) + + val aligner: Aligner[Proposal, ProposalPropertiesInput] = + Aligner( + undoCtx, + UpdateProposalInput( + programId = props.programId.assign, + SET = ProposalPropertiesInput() + ), + (ProposalQueriesGQL.UpdateProposalMutation[IO].execute(_)).andThen(_.void) + ).zoom(Iso.id[Proposal].asLens, UpdateProposalInput.SET.modify) + + val abstractAligner: Aligner[Option[NonEmptyString], Input[NonEmptyString]] = + aligner.zoom(Proposal.abstrakt, ProposalPropertiesInput.`abstract`.modify) + + val abstractView: View[Option[NonEmptyString]] = abstractAligner.view(_.orUnassign) + + val defaultLayouts = ExploreGridLayouts.sectionLayout(GridLayoutSection.ProposalLayout) + + val detailsTile = + Tile(ProposalTabTileIds.DetailsId.id, "Details")( + _ => + ProposalDetailsBody( + props.proposal, + aligner, + props.timeEstimateRange, + props.cfps, + props.readonly + ), + (_, s) => ProposalDetailsTitle(undoCtx, s) + ) + + val usersTile = + ProgramUsers.programUsersTile( + props.programId, + props.readonly, + props.users, + props.invitations, + overlayRef + ) + + val abstractTile = + Tile( + ProposalTabTileIds.AbstractId.id, + "Abstract", + bodyClass = ExploreStyles.ProposalAbstract + )(_ => + FormInputTextAreaView( + id = "abstract".refined, + value = abstractView.as(OptionNonEmptyStringIso) + )(^.disabled := props.readonly) + ) + + val attachmentsTile = + Tile(ProposalTabTileIds.AttachmentsId.id, "Attachments")(_ => + props.authToken.map(token => + ProposalAttachmentsTable(props.programId, token, props.attachments, props.readonly) + ) + ) + + <.div(ExploreStyles.MultiPanelTile)( + InviteUserPopup(props.programId, props.invitations, overlayRef), + TileController( + props.optUserId, + resize.width.getOrElse(1), + defaultLayouts, + props.layout, + List(detailsTile, usersTile, abstractTile, attachmentsTile), + GridLayoutSection.ProposalLayout, + storeLayout = true + ) + ).withRef(resize.ref) From dadab12721ee77f259e365d3e2316daf7fe94784 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ra=C3=BAl=20Piaggio?= Date: Mon, 16 Sep 2024 10:43:34 -0300 Subject: [PATCH 02/11] more refactor --- explore/src/main/scala/explore/proposal/ProgramUsers.scala | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/explore/src/main/scala/explore/proposal/ProgramUsers.scala b/explore/src/main/scala/explore/proposal/ProgramUsers.scala index f2f5cc0aea..88ea8b6ef1 100644 --- a/explore/src/main/scala/explore/proposal/ProgramUsers.scala +++ b/explore/src/main/scala/explore/proposal/ProgramUsers.scala @@ -4,12 +4,10 @@ package explore.proposal import cats.data.NonEmptyList -import cats.effect.IO import cats.syntax.all.* import crystal.react.* import explore.Icons import explore.components.Tile -import explore.model.AppContext import explore.model.CoIInvitation import explore.model.ProgramUserWithRole import explore.model.ProposalTabTileIds @@ -79,8 +77,5 @@ object ProgramUsers: "Pending invitations", ProgramUserInvitations(props.invitations, props.readOnly) ) - .when( - props.invitations - .when(_.filter(_.status === InvitationStatus.Pending).nonEmpty) - ) + .when(props.invitations.get.filter(_.status === InvitationStatus.Pending).nonEmpty) ) From eaa86af7c1944bf50ad1ab8dbbc74de90148c55f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ra=C3=BAl=20Piaggio?= Date: Mon, 16 Sep 2024 11:41:49 -0300 Subject: [PATCH 03/11] first steps towards support users table --- .../scala/explore/programs/SupportUsers.scala | 24 ++++++++++++++ .../explore/proposal/ProgramUsersTable.scala | 32 +++++++++---------- 2 files changed, 40 insertions(+), 16 deletions(-) create mode 100644 explore/src/main/scala/explore/programs/SupportUsers.scala diff --git a/explore/src/main/scala/explore/programs/SupportUsers.scala b/explore/src/main/scala/explore/programs/SupportUsers.scala new file mode 100644 index 0000000000..9472e9281e --- /dev/null +++ b/explore/src/main/scala/explore/programs/SupportUsers.scala @@ -0,0 +1,24 @@ +// 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 japgolly.scalajs.react.* +import japgolly.scalajs.react.vdom.html_<^.* +import lucuma.react.common.ReactFnProps +import crystal.react.View +import explore.model.ProgramUserWithRole +import lucuma.core.model.Program +import explore.proposal.ProgramUsersTable + +case class SupportUsers( + programId: Program.Id, + users: View[List[ProgramUserWithRole]], + readonly: Boolean +) extends ReactFnProps(SupportUsers.component) + +object SupportUsers: + private type Props = SupportUsers + + private val component = ScalaFnComponent[Props]: props => + ProgramUsersTable(props.programId, props.users, props.readonly) diff --git a/explore/src/main/scala/explore/proposal/ProgramUsersTable.scala b/explore/src/main/scala/explore/proposal/ProgramUsersTable.scala index c527e39525..03e8b649b4 100644 --- a/explore/src/main/scala/explore/proposal/ProgramUsersTable.scala +++ b/explore/src/main/scala/explore/proposal/ProgramUsersTable.scala @@ -36,6 +36,7 @@ import lucuma.react.table.* import lucuma.refined.* import lucuma.schemas.ObservationDB.Types.UnlinkUserInput import lucuma.ui.primereact.* +import lucuma.ui.primereact.given import lucuma.ui.syntax.all.given import lucuma.ui.table.* import lucuma.ui.utils.* @@ -150,17 +151,16 @@ object ProgramUsersTable: enableResizing = true, cell = c => c.table.options.meta.map: meta => + val cell: View[ProgramUserWithRole] = c.row.original + val userId: User.Id = cell.get.user.id + val usersView: View[Option[PartnerLink]] = + c.value.withOnMod: pl => + updateProgramPartner[IO](meta.programId, userId, pl).runAsyncAndForget + val pl: Option[PartnerLink] = + cell.get.partnerLink.flatMap: + case PartnerLink.HasUnspecifiedPartner => None + case p => Some(p) - val cell = c.row.original - val userId = cell.get.user.id - val usersView = c.value.withOnMod(pl => - updateProgramPartner[IO](meta.programId, userId, pl).runAsyncAndForget - ) - - val pl = cell.get.partnerLink.flatMap { - case PartnerLink.HasUnspecifiedPartner => None - case p => Some(p) - } partnerSelector(pl, usersView.set, meta.readOnly || meta.isActive.get.value) ), column(EmailColumnId, _.get.user.profile.foldMap(_.primaryEmail).getOrElse("-")), @@ -174,19 +174,19 @@ object ProgramUsersTable: val cell = c.row.original val userId = cell.get.user.id c.table.options.meta.map: meta => - val view = c.value - .withOnMod(es => updateUserES[IO](meta.programId, userId, es).runAsyncAndForget) + val view: View[Option[EducationalStatus]] = + c.value.withOnMod: es => + updateUserES[IO](meta.programId, userId, es).runAsyncAndForget - EnumOptionalDropdown[EducationalStatus]( + EnumDropdownOptionalView( id = "es".refined, - value = view.get, + value = view, showClear = true, itemTemplate = _.value.shortName, valueTemplate = _.value.shortName, emptyMessageTemplate = "No Selection", disabled = meta.readOnly || meta.isActive.get.value, - clazz = ExploreStyles.PartnerSelector, - onChange = view.set + clazz = ExploreStyles.PartnerSelector ) ), ColDef( From db220e04c5d723ce9a88a23d65124fc3ccf0bf83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ra=C3=BAl=20Piaggio?= Date: Tue, 17 Sep 2024 21:01:04 -0300 Subject: [PATCH 04/11] unify roles, invite any role, update deps --- .../scala/explore/model/reusability.scala | 2 +- .../queries/common/InvitationQueriesGQL.scala | 9 ++++--- .../common/ProgramInvitationsSubquery.scala | 5 ++-- .../explore/programs/ProgramDetailsTile.scala | 14 +++++----- .../scala/explore/programs/SupportUsers.scala | 17 ++++++++---- .../explore/proposal/InviteUserPopup.scala | 21 ++++++++------- .../scala/explore/proposal/ProgramUsers.scala | 26 ++++++++++++++----- .../explore/proposal/ProposalEditor.scala | 7 ++--- .../ProgramUserInvitations.scala | 24 ++++++++--------- .../ProgramUsersTable.scala | 18 ++++++++----- .../scala/explore/model/ProgramDetails.scala | 6 ++--- .../explore/model/ProgramUserWithRole.scala | 14 +++++----- ...IInvitation.scala => UserInvitation.scala} | 11 +++++--- .../explore/model/enums/ProgramUserRole.scala | 13 ---------- project/Versions.scala | 2 +- 15 files changed, 103 insertions(+), 86 deletions(-) rename explore/src/main/scala/explore/{proposal => users}/ProgramUserInvitations.scala (87%) rename explore/src/main/scala/explore/{proposal => users}/ProgramUsersTable.scala (95%) rename model/shared/src/main/scala/explore/model/{CoIInvitation.scala => UserInvitation.scala} (78%) delete mode 100644 model/shared/src/main/scala/explore/model/enums/ProgramUserRole.scala diff --git a/common/src/main/scala/explore/model/reusability.scala b/common/src/main/scala/explore/model/reusability.scala index 0e2079dd85..962197c892 100644 --- a/common/src/main/scala/explore/model/reusability.scala +++ b/common/src/main/scala/explore/model/reusability.scala @@ -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 diff --git a/explore/src/clue/scala/queries/common/InvitationQueriesGQL.scala b/explore/src/clue/scala/queries/common/InvitationQueriesGQL.scala index d7a0bbbfe1..616622149d 100644 --- a/explore/src/clue/scala/queries/common/InvitationQueriesGQL.scala +++ b/explore/src/clue/scala/queries/common/InvitationQueriesGQL.scala @@ -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 } @@ -34,7 +35,7 @@ object InvitationQueriesGQL: object Data: object CreateUserInvitation: - type Invitation = CoIInvitation + type Invitation = UserInvitation @GraphQL trait RevokeInvitationMutation extends GraphQLOperation[ObservationDB]: diff --git a/explore/src/clue/scala/queries/common/ProgramInvitationsSubquery.scala b/explore/src/clue/scala/queries/common/ProgramInvitationsSubquery.scala index 858ea2ee5f..4b15e94462 100644 --- a/explore/src/clue/scala/queries/common/ProgramInvitationsSubquery.scala +++ b/explore/src/clue/scala/queries/common/ProgramInvitationsSubquery.scala @@ -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 diff --git a/explore/src/main/scala/explore/programs/ProgramDetailsTile.scala b/explore/src/main/scala/explore/programs/ProgramDetailsTile.scala index 9209781f90..c285aa79b4 100644 --- a/explore/src/main/scala/explore/programs/ProgramDetailsTile.scala +++ b/explore/src/main/scala/explore/programs/ProgramDetailsTile.scala @@ -10,7 +10,7 @@ import explore.model.Constants import explore.model.ProgramDetails import explore.model.ProgramTimes import explore.model.ProgramUserWithRole -import explore.model.enums.ProgramUserRole +import lucuma.core.enums.ProgramUserRole import japgolly.scalajs.react.* import japgolly.scalajs.react.vdom.html_<^.* import lucuma.core.model.Semester @@ -28,10 +28,10 @@ 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)) + val details: ProgramDetails = props.programDetails + val thesis: Boolean = details.allUsers.exists(_.thesis.exists(_ === true)) + val supportPrimary: List[ProgramUserWithRole] = + details.allUsers.filter(_.role === ProgramUserRole.SupportPrimary) <.div(ExploreStyles.ProgramDetailsTile)( <.div(ExploreStyles.ProgramDetailsInfoArea)( @@ -49,8 +49,8 @@ object ProgramDetailsTile: ), <.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(supportPrimary.map(_.nameWithEmail).mkString(", "), "Contact Scientists") + .when(supportPrimary.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) diff --git a/explore/src/main/scala/explore/programs/SupportUsers.scala b/explore/src/main/scala/explore/programs/SupportUsers.scala index 9472e9281e..312015e625 100644 --- a/explore/src/main/scala/explore/programs/SupportUsers.scala +++ b/explore/src/main/scala/explore/programs/SupportUsers.scala @@ -3,13 +3,15 @@ package explore.programs -import japgolly.scalajs.react.* -import japgolly.scalajs.react.vdom.html_<^.* -import lucuma.react.common.ReactFnProps +import cats.data.NonEmptySet import crystal.react.View import explore.model.ProgramUserWithRole +import explore.users.ProgramUsersTable +import japgolly.scalajs.react.* +import japgolly.scalajs.react.vdom.html_<^.* import lucuma.core.model.Program -import explore.proposal.ProgramUsersTable +import lucuma.react.common.ReactFnProps +import lucuma.core.enums.ProgramUserRole case class SupportUsers( programId: Program.Id, @@ -21,4 +23,9 @@ object SupportUsers: private type Props = SupportUsers private val component = ScalaFnComponent[Props]: props => - ProgramUsersTable(props.programId, props.users, props.readonly) + ProgramUsersTable( + props.programId, + props.users, + NonEmptySet.of(ProgramUserRole.SupportPrimary, ProgramUserRole.SupportSecondary), + props.readonly + ) diff --git a/explore/src/main/scala/explore/proposal/InviteUserPopup.scala b/explore/src/main/scala/explore/proposal/InviteUserPopup.scala index 07c93fa04b..8dced2e08c 100644 --- a/explore/src/main/scala/explore/proposal/InviteUserPopup.scala +++ b/explore/src/main/scala/explore/proposal/InviteUserPopup.scala @@ -15,11 +15,12 @@ import eu.timepit.refined.types.string.NonEmptyString import explore.Icons import explore.components.ui.ExploreStyles import explore.model.AppContext -import explore.model.CoIInvitation +import explore.model.UserInvitation import japgolly.scalajs.react.* import japgolly.scalajs.react.vdom.html_<^.* import lucuma.core.data.EmailAddress import lucuma.core.data.EmailPred +import lucuma.core.enums.ProgramUserRole import lucuma.core.model.Program import lucuma.core.validation.InputValidSplitEpi import lucuma.react.common.ReactFnProps @@ -40,12 +41,13 @@ import queries.common.InvitationQueriesGQL.CreateInviteMutation.Data case class InviteUserPopup( pid: Program.Id, - invitations: View[List[CoIInvitation]], + role: ProgramUserRole, + invitations: View[List[UserInvitation]], ref: OverlayPanelRef ) extends ReactFnProps(InviteUserPopup.component) object InviteUserPopup: - val MailValidator: InputValidSplitEpi[EmailAddress] = + private val MailValidator: InputValidSplitEpi[EmailAddress] = // Scala doesn't like type aliases with refined types? InputValidSplitEpi.refinedString[EmailPred].asInstanceOf[InputValidSplitEpi[EmailAddress]] @@ -69,8 +71,8 @@ object InviteUserPopup: viewKey: View[Option[String]] ): IO[Unit] = (createInvite.set(CreateInviteProcess.Running).to[IO] *> - CreateInviteMutation[IO].execute(pid, email.value.value)).attempt - .flatMap { + CreateInviteMutation[IO].execute(pid, email.value.value, props.role)).attempt + .flatMap: case Left(e) => Logger[IO].error(e)("Error creating invitation") *> createInvite.set(CreateInviteProcess.Error).to[IO] @@ -78,14 +80,12 @@ object InviteUserPopup: props.invitations.mod(r.createUserInvitation.invitation :: _).to[IO] *> viewKey.set(r.createUserInvitation.key.some).to[IO] *> createInvite.set(CreateInviteProcess.Done).to[IO] - } OverlayPanel( closeOnEscape = true, onHide = key.set(None) >> emailView.set(None).runAsyncAndForget )( - <.div( - PrimeStyles.Dialog, + <.div(PrimeStyles.Dialog)( <.div(PrimeStyles.DialogHeader, "Create CoI invitation"), <.div(PrimeStyles.DialogContent)( <.div(LucumaPrimeStyles.FormColumnCompact)( @@ -110,8 +110,9 @@ object InviteUserPopup: ) ), <.div(PrimeStyles.DialogFooter)( - Message(text = "Error submitting user invite, try later", - severity = Message.Severity.Error + Message( + text = "Error submitting user invite, try later", + severity = Message.Severity.Error ).when(inviteState.get === CreateInviteProcess.Error), Button( icon = Icons.Close, diff --git a/explore/src/main/scala/explore/proposal/ProgramUsers.scala b/explore/src/main/scala/explore/proposal/ProgramUsers.scala index 88ea8b6ef1..4e0ec0d62e 100644 --- a/explore/src/main/scala/explore/proposal/ProgramUsers.scala +++ b/explore/src/main/scala/explore/proposal/ProgramUsers.scala @@ -3,14 +3,17 @@ package explore.proposal +import cats.data.NonEmptySet import cats.data.NonEmptyList import cats.syntax.all.* import crystal.react.* import explore.Icons import explore.components.Tile -import explore.model.CoIInvitation import explore.model.ProgramUserWithRole import explore.model.ProposalTabTileIds +import explore.model.UserInvitation +import explore.users.ProgramUserInvitations +import explore.users.ProgramUsersTable import japgolly.scalajs.react.* import japgolly.scalajs.react.vdom.html_<^.* import lucuma.core.enums.InvitationStatus @@ -22,6 +25,7 @@ import lucuma.react.primereact.Button import lucuma.react.primereact.OverlayPanelRef import lucuma.ui.primereact.* import lucuma.ui.syntax.all.given +import lucuma.core.enums.ProgramUserRole enum CreateInviteProcess(private val tag: String) derives Enumerated: case Idle extends CreateInviteProcess("idle") @@ -36,13 +40,18 @@ case class ProgramUsers( pid: Program.Id, readOnly: Boolean, users: View[List[ProgramUserWithRole]], - invitations: View[List[CoIInvitation]], + invitations: View[List[UserInvitation]], state: View[ProgramUsersState] ) extends ReactFnProps(ProgramUsers.component) object ProgramUsers: + private type Props = ProgramUsers - def inviteControl(readOnly: Boolean, ref: OverlayPanelRef, state: View[ProgramUsersState]) = + private def inviteControl( + readOnly: Boolean, + ref: OverlayPanelRef, + state: View[ProgramUsersState] + ) = Button( severity = Button.Severity.Secondary, size = Button.Size.Small, @@ -57,7 +66,7 @@ object ProgramUsers: pid: Program.Id, readOnly: Boolean, users: View[List[ProgramUserWithRole]], - invitations: View[List[CoIInvitation]], + invitations: View[List[UserInvitation]], ref: OverlayPanelRef ) = Tile( @@ -66,12 +75,15 @@ object ProgramUsers: ProgramUsersState(CreateInviteProcess.Idle) )(ProgramUsers(pid, readOnly, users, invitations, _), (s, _) => inviteControl(readOnly, ref, s)) - private type Props = ProgramUsers - private val component = ScalaFnComponent[Props]: props => <.div( - ProgramUsersTable(props.pid, props.users, props.readOnly), + ProgramUsersTable( + props.pid, + props.users, + NonEmptySet.of(ProgramUserRole.Pi, ProgramUserRole.Coi, ProgramUserRole.CoiRO), + props.readOnly + ), React .Fragment( "Pending invitations", diff --git a/explore/src/main/scala/explore/proposal/ProposalEditor.scala b/explore/src/main/scala/explore/proposal/ProposalEditor.scala index 2bf0f3a19f..f3f9bb4e0a 100644 --- a/explore/src/main/scala/explore/proposal/ProposalEditor.scala +++ b/explore/src/main/scala/explore/proposal/ProposalEditor.scala @@ -16,18 +16,19 @@ import explore.components.TileController import explore.components.ui.* import explore.model.AppContext import explore.model.CallForProposal -import explore.model.CoIInvitation import explore.model.ExploreGridLayouts import explore.model.ProgramTimeRange import explore.model.ProgramUserWithRole import explore.model.Proposal import explore.model.ProposalAttachment import explore.model.ProposalTabTileIds +import explore.model.UserInvitation import explore.model.enums.GridLayoutSection import explore.model.layout.LayoutsMap import explore.undo.* import japgolly.scalajs.react.* import japgolly.scalajs.react.vdom.html_<^.* +import lucuma.core.enums.ProgramUserRole import lucuma.core.model.Program import lucuma.core.model.User import lucuma.react.common.ReactFnProps @@ -50,7 +51,7 @@ case class ProposalEditor( undoStacks: View[UndoStacks[IO, Proposal]], timeEstimateRange: Pot[Option[ProgramTimeRange]], users: View[List[ProgramUserWithRole]], - invitations: View[List[CoIInvitation]], + invitations: View[List[UserInvitation]], attachments: View[List[ProposalAttachment]], authToken: Option[NonEmptyString], cfps: List[CallForProposal], @@ -157,7 +158,7 @@ object ProposalEditor: ) <.div(ExploreStyles.MultiPanelTile)( - InviteUserPopup(props.programId, props.invitations, overlayRef), + InviteUserPopup(props.programId, ProgramUserRole.Coi, props.invitations, overlayRef), TileController( props.optUserId, resize.width.getOrElse(1), diff --git a/explore/src/main/scala/explore/proposal/ProgramUserInvitations.scala b/explore/src/main/scala/explore/users/ProgramUserInvitations.scala similarity index 87% rename from explore/src/main/scala/explore/proposal/ProgramUserInvitations.scala rename to explore/src/main/scala/explore/users/ProgramUserInvitations.scala index bba5d3490b..b96ad868e0 100644 --- a/explore/src/main/scala/explore/proposal/ProgramUserInvitations.scala +++ b/explore/src/main/scala/explore/users/ProgramUserInvitations.scala @@ -1,7 +1,7 @@ // 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 +package explore.users import cats.effect.IO import cats.syntax.all.* @@ -12,8 +12,8 @@ import explore.Icons import explore.components.* import explore.components.ui.ExploreStyles import explore.model.AppContext -import explore.model.CoIInvitation import explore.model.IsActive +import explore.model.UserInvitation import explore.model.reusability.given import japgolly.scalajs.react.* import japgolly.scalajs.react.vdom.html_<^.* @@ -29,7 +29,7 @@ import lucuma.ui.syntax.all.given import lucuma.ui.table.* import queries.common.InvitationQueriesGQL.* -case class ProgramUserInvitations(invitations: View[List[CoIInvitation]], readOnly: Boolean) +case class ProgramUserInvitations(invitations: View[List[UserInvitation]], readOnly: Boolean) extends ReactFnProps(ProgramUserInvitations.component) object ProgramUserInvitations: @@ -37,11 +37,11 @@ object ProgramUserInvitations: private case class TableMeta( isActive: View[IsActive], - invitations: View[List[CoIInvitation]], + invitations: View[List[UserInvitation]], readOnly: Boolean ) - private val ColDef = ColumnDef.WithTableMeta[CoIInvitation, TableMeta] + private val ColDef = ColumnDef.WithTableMeta[UserInvitation, TableMeta] private val KeyId: ColumnId = ColumnId("id") private val EmailId: ColumnId = ColumnId("email") @@ -57,13 +57,13 @@ object ProgramUserInvitations: private def column[V]( id: ColumnId, - accessor: CoIInvitation => V - ): ColumnDef.Single.WithTableMeta[CoIInvitation, V, TableMeta] = + accessor: UserInvitation => V + ): ColumnDef.Single.WithTableMeta[UserInvitation, V, TableMeta] = ColDef(id, accessor, columnNames(id)) private def columns( ctx: AppContext[IO] - ): List[ColumnDef.WithTableMeta[CoIInvitation, ?, TableMeta]] = + ): List[ColumnDef.WithTableMeta[UserInvitation, ?, TableMeta]] = import ctx.given List( @@ -73,11 +73,9 @@ object ProgramUserInvitations: EmailStatusId, _.emailStatus, "Email Status", - cell = { cell => - cell.value - .map(es => <.span(es.tag.toUpperCase).withTooltip(es.description)) - .getOrElse(<.span()) - } + cell = _.value + .map(es => <.span(es.tag.toUpperCase).withTooltip(es.description)) + .getOrElse(<.span()) ), ColDef( RevokeId, diff --git a/explore/src/main/scala/explore/proposal/ProgramUsersTable.scala b/explore/src/main/scala/explore/users/ProgramUsersTable.scala similarity index 95% rename from explore/src/main/scala/explore/proposal/ProgramUsersTable.scala rename to explore/src/main/scala/explore/users/ProgramUsersTable.scala index 03e8b649b4..6c0001dbd0 100644 --- a/explore/src/main/scala/explore/proposal/ProgramUsersTable.scala +++ b/explore/src/main/scala/explore/users/ProgramUsersTable.scala @@ -1,8 +1,9 @@ // 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 +package explore.users +import cats.data.NonEmptySet import cats.effect.IO import cats.syntax.all.* import crystal.react.* @@ -42,11 +43,13 @@ import lucuma.ui.table.* import lucuma.ui.utils.* import monocle.function.Each.* import queries.common.ProposalQueriesGQL.UnlinkUser +import lucuma.core.enums.ProgramUserRole case class ProgramUsersTable( - programId: Program.Id, - users: View[List[ProgramUserWithRole]], - readOnly: Boolean + programId: Program.Id, + users: View[List[ProgramUserWithRole]], + filterRoles: NonEmptySet[ProgramUserRole], + readOnly: Boolean ) extends ReactFnProps(ProgramUsersTable.component) object ProgramUsersTable: @@ -260,7 +263,8 @@ object ProgramUsersTable: severity = Button.Severity.Secondary, disabled = meta.readOnly || meta.isActive.get.value, onClick = unlink - ).mini.compact.unless(cell.value.get.role.isEmpty) // don't allow removing the PI + ).mini.compact + .unless(cell.value.get.role === ProgramUserRole.Pi) // don't allow removing the PI ) , size = 35.toPx @@ -274,8 +278,8 @@ object ProgramUsersTable: .useStateView(IsActive(false)) .useMemoBy((_, _, _) => ()): (_, ctx, _) => // cols _ => columns(ctx) - .useMemoBy((props, _, _, _) => props.users.reuseByValue): (p, _, _, _) => // rows - _.toListOfViews + .useMemoBy((props, _, _, _) => props.users.reuseByValue): (props, _, _, _) => // rows + _.toListOfViews.filter(row => props.filterRoles.contains_(row.get.role)) .useReactTableBy: (props, _, isActive, cols, rows) => TableOptions( cols, diff --git a/model/shared/src/main/scala/explore/model/ProgramDetails.scala b/model/shared/src/main/scala/explore/model/ProgramDetails.scala index 932e9bcc21..f64e425990 100644 --- a/model/shared/src/main/scala/explore/model/ProgramDetails.scala +++ b/model/shared/src/main/scala/explore/model/ProgramDetails.scala @@ -21,7 +21,7 @@ case class ProgramDetails( proposalStatus: ProposalStatus, pi: Option[ProgramUserWithRole], users: List[ProgramUserWithRole], - invitations: List[CoIInvitation], + invitations: List[UserInvitation], reference: Option[ProgramReference], allocations: CategoryAllocationList ) derives Eq: @@ -30,7 +30,7 @@ case class ProgramDetails( object ProgramDetails: val proposal: Lens[ProgramDetails, Option[Proposal]] = Focus[ProgramDetails](_.proposal) val proposalStatus: Lens[ProgramDetails, ProposalStatus] = Focus[ProgramDetails](_.proposalStatus) - val invitations: Lens[ProgramDetails, List[CoIInvitation]] = Focus[ProgramDetails](_.invitations) + val invitations: Lens[ProgramDetails, List[UserInvitation]] = Focus[ProgramDetails](_.invitations) val allUsers: Lens[ProgramDetails, List[ProgramUserWithRole]] = Lens[ProgramDetails, List[ProgramUserWithRole]](_.allUsers)(a => b => b.copy(pi = a.headOption, users = a.tail) @@ -47,7 +47,7 @@ object ProgramDetails: ps <- c.get[ProposalStatus]("proposalStatus") pi <- c.downField("pi").as[Option[ProgramUserWithRole]] us <- c.get[List[ProgramUserWithRole]]("users") - in <- c.get[List[CoIInvitation]]("userInvitations") + in <- c.get[List[UserInvitation]]("userInvitations") r <- c.downField("reference").downField("label").success.traverse(_.as[Option[ProgramReference]]) as <- c.downField("allocations").as[CategoryAllocationList] diff --git a/model/shared/src/main/scala/explore/model/ProgramUserWithRole.scala b/model/shared/src/main/scala/explore/model/ProgramUserWithRole.scala index 2dfdda9a70..5721f233b3 100644 --- a/model/shared/src/main/scala/explore/model/ProgramUserWithRole.scala +++ b/model/shared/src/main/scala/explore/model/ProgramUserWithRole.scala @@ -6,7 +6,7 @@ package explore.model import cats.Eq import cats.derived.* import cats.syntax.all.* -import explore.model.enums.ProgramUserRole +import lucuma.core.enums.ProgramUserRole import io.circe.Decoder import lucuma.core.enums.EducationalStatus import lucuma.core.enums.Gender @@ -19,16 +19,18 @@ import monocle.Lens case class ProgramUserWithRole( user: ProgramUser, partnerLink: Option[PartnerLink], - role: Option[ProgramUserRole], + // role: Option[ProgramUserRole], + role: ProgramUserRole, educationalStatus: Option[EducationalStatus], thesis: Option[Boolean], gender: Option[Gender] ) derives Eq: export user.{name, nameWithEmail} - lazy val roleName: String = role match - case None => "Pi" - case Some(role) => role.tag + lazy val roleName: String = role.tag + // role match + // case None => "Pi" + // case Some(role) => role.tag object ProgramUserWithRole: val user: Lens[ProgramUserWithRole, ProgramUser] = Focus[ProgramUserWithRole](_.user) @@ -49,7 +51,7 @@ object ProgramUserWithRole: for { u <- c.downField("user").as[ProgramUser] pl <- c.downField("partnerLink").as[Option[PartnerLink]] - role <- c.downField("role").as[Option[ProgramUserRole]] + role <- c.downField("role").as[ProgramUserRole] es <- c.downField("educationalStatus").as[Option[EducationalStatus]] th <- c.downField("thesis").as[Option[Boolean]] g <- c.downField("gender").as[Option[Gender]] diff --git a/model/shared/src/main/scala/explore/model/CoIInvitation.scala b/model/shared/src/main/scala/explore/model/UserInvitation.scala similarity index 78% rename from model/shared/src/main/scala/explore/model/CoIInvitation.scala rename to model/shared/src/main/scala/explore/model/UserInvitation.scala index d136c4f3a2..3b26015c48 100644 --- a/model/shared/src/main/scala/explore/model/CoIInvitation.scala +++ b/model/shared/src/main/scala/explore/model/UserInvitation.scala @@ -13,20 +13,23 @@ import io.circe.refined.* import lucuma.core.data.EmailAddress import lucuma.core.enums.EmailStatus import lucuma.core.enums.InvitationStatus +import lucuma.core.enums.ProgramUserRole import lucuma.core.util.Enumerated -case class CoIInvitation( +case class UserInvitation( id: String, email: EmailAddress, + role: ProgramUserRole, status: InvitationStatus, emailStatus: Option[EmailStatus] ) derives Eq -object CoIInvitation: - given Decoder[CoIInvitation] = c => +object UserInvitation: + given Decoder[UserInvitation] = c => for { id <- c.get[String]("id") em <- c.get[EmailAddress]("recipientEmail") + r <- c.get[ProgramUserRole]("role") s <- c.get[InvitationStatus]("status") es <- c.downField("email").downField("status").success.traverse(_.as[EmailStatus]) - } yield CoIInvitation(id, em, s, es) + } yield UserInvitation(id, em, r, s, es) diff --git a/model/shared/src/main/scala/explore/model/enums/ProgramUserRole.scala b/model/shared/src/main/scala/explore/model/enums/ProgramUserRole.scala deleted file mode 100644 index 8b5a74d682..0000000000 --- a/model/shared/src/main/scala/explore/model/enums/ProgramUserRole.scala +++ /dev/null @@ -1,13 +0,0 @@ -// 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.model.enums - -import lucuma.core.util.Enumerated - -enum ProgramUserRole(val tag: String) derives Enumerated { - case Pi extends ProgramUserRole("Pi") - case Coi extends ProgramUserRole("Coi") - case Observer extends ProgramUserRole("Observer") - case Support extends ProgramUserRole("Support") -} diff --git a/project/Versions.scala b/project/Versions.scala index 930e76cf50..f6d332b381 100644 --- a/project/Versions.scala +++ b/project/Versions.scala @@ -26,7 +26,7 @@ object Versions { val lucumaITC = "0.22.3" val lucumaReact = "0.71.1" val lucumaRefined = "0.1.3" - val lucumaSchemas = "0.99.1" + val lucumaSchemas = "0.100.0" val lucumaOdbSchema = "0.13.1" val lucumaSSO = "0.6.24" val lucumaUI = "0.117.0" From 03e4d36e64cc2035da2ff123cde8f444b8933b00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ra=C3=BAl=20Piaggio?= Date: Wed, 18 Sep 2024 09:05:40 -0300 Subject: [PATCH 05/11] scalafix, remove comments --- .../main/scala/explore/programs/ProgramDetailsTile.scala | 2 +- explore/src/main/scala/explore/programs/SupportUsers.scala | 2 +- explore/src/main/scala/explore/proposal/ProgramUsers.scala | 4 ++-- .../src/main/scala/explore/users/ProgramUsersTable.scala | 2 +- .../src/main/scala/explore/model/ProgramUserWithRole.scala | 7 +------ 5 files changed, 6 insertions(+), 11 deletions(-) diff --git a/explore/src/main/scala/explore/programs/ProgramDetailsTile.scala b/explore/src/main/scala/explore/programs/ProgramDetailsTile.scala index c285aa79b4..65df687d79 100644 --- a/explore/src/main/scala/explore/programs/ProgramDetailsTile.scala +++ b/explore/src/main/scala/explore/programs/ProgramDetailsTile.scala @@ -10,9 +10,9 @@ import explore.model.Constants import explore.model.ProgramDetails import explore.model.ProgramTimes import explore.model.ProgramUserWithRole -import lucuma.core.enums.ProgramUserRole import japgolly.scalajs.react.* import japgolly.scalajs.react.vdom.html_<^.* +import lucuma.core.enums.ProgramUserRole import lucuma.core.model.Semester import lucuma.core.syntax.display.* import lucuma.react.common.ReactFnProps diff --git a/explore/src/main/scala/explore/programs/SupportUsers.scala b/explore/src/main/scala/explore/programs/SupportUsers.scala index 312015e625..24a57fb885 100644 --- a/explore/src/main/scala/explore/programs/SupportUsers.scala +++ b/explore/src/main/scala/explore/programs/SupportUsers.scala @@ -9,9 +9,9 @@ import explore.model.ProgramUserWithRole 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 -import lucuma.core.enums.ProgramUserRole case class SupportUsers( programId: Program.Id, diff --git a/explore/src/main/scala/explore/proposal/ProgramUsers.scala b/explore/src/main/scala/explore/proposal/ProgramUsers.scala index 4e0ec0d62e..0ec0cc57c1 100644 --- a/explore/src/main/scala/explore/proposal/ProgramUsers.scala +++ b/explore/src/main/scala/explore/proposal/ProgramUsers.scala @@ -3,8 +3,8 @@ package explore.proposal -import cats.data.NonEmptySet import cats.data.NonEmptyList +import cats.data.NonEmptySet import cats.syntax.all.* import crystal.react.* import explore.Icons @@ -17,6 +17,7 @@ import explore.users.ProgramUsersTable import japgolly.scalajs.react.* import japgolly.scalajs.react.vdom.html_<^.* import lucuma.core.enums.InvitationStatus +import lucuma.core.enums.ProgramUserRole import lucuma.core.model.Program import lucuma.core.util.Enumerated import lucuma.core.util.NewType @@ -25,7 +26,6 @@ import lucuma.react.primereact.Button import lucuma.react.primereact.OverlayPanelRef import lucuma.ui.primereact.* import lucuma.ui.syntax.all.given -import lucuma.core.enums.ProgramUserRole enum CreateInviteProcess(private val tag: String) derives Enumerated: case Idle extends CreateInviteProcess("idle") diff --git a/explore/src/main/scala/explore/users/ProgramUsersTable.scala b/explore/src/main/scala/explore/users/ProgramUsersTable.scala index 6c0001dbd0..6bb21dec45 100644 --- a/explore/src/main/scala/explore/users/ProgramUsersTable.scala +++ b/explore/src/main/scala/explore/users/ProgramUsersTable.scala @@ -23,6 +23,7 @@ import japgolly.scalajs.react.vdom.html_<^.* import lucuma.core.enums.EducationalStatus import lucuma.core.enums.Gender import lucuma.core.enums.Partner +import lucuma.core.enums.ProgramUserRole import lucuma.core.model.PartnerLink import lucuma.core.model.Program import lucuma.core.model.User @@ -43,7 +44,6 @@ import lucuma.ui.table.* import lucuma.ui.utils.* import monocle.function.Each.* import queries.common.ProposalQueriesGQL.UnlinkUser -import lucuma.core.enums.ProgramUserRole case class ProgramUsersTable( programId: Program.Id, diff --git a/model/shared/src/main/scala/explore/model/ProgramUserWithRole.scala b/model/shared/src/main/scala/explore/model/ProgramUserWithRole.scala index 5721f233b3..68da90884f 100644 --- a/model/shared/src/main/scala/explore/model/ProgramUserWithRole.scala +++ b/model/shared/src/main/scala/explore/model/ProgramUserWithRole.scala @@ -6,20 +6,18 @@ package explore.model import cats.Eq import cats.derived.* import cats.syntax.all.* -import lucuma.core.enums.ProgramUserRole import io.circe.Decoder import lucuma.core.enums.EducationalStatus import lucuma.core.enums.Gender +import lucuma.core.enums.ProgramUserRole import lucuma.core.model.PartnerLink import lucuma.odb.json.partnerlink.given import monocle.Focus import monocle.Lens -// an empty role implies PI case class ProgramUserWithRole( user: ProgramUser, partnerLink: Option[PartnerLink], - // role: Option[ProgramUserRole], role: ProgramUserRole, educationalStatus: Option[EducationalStatus], thesis: Option[Boolean], @@ -28,9 +26,6 @@ case class ProgramUserWithRole( export user.{name, nameWithEmail} lazy val roleName: String = role.tag - // role match - // case None => "Pi" - // case Some(role) => role.tag object ProgramUserWithRole: val user: Lens[ProgramUserWithRole, ProgramUser] = Focus[ProgramUserWithRole](_.user) From a1ef822deb9380084811d85ca8529ac01c62a2e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ra=C3=BAl=20Piaggio?= Date: Wed, 18 Sep 2024 09:24:36 -0300 Subject: [PATCH 06/11] some more refactoring --- .../scala/explore/proposal/CallDeadline.scala | 43 +++ ...ramUsers.scala => InvestigatorUsers.scala} | 35 +- .../explore/proposal/ProposalEditor.scala | 3 +- .../proposal/ProposalTabContents.scala | 308 +++++++----------- .../explore/users/CreateInviteProcess.scala | 12 + .../{proposal => users}/InviteUserPopup.scala | 2 +- .../users/ProgramUserInvitations.scala | 4 +- 7 files changed, 197 insertions(+), 210 deletions(-) create mode 100644 explore/src/main/scala/explore/proposal/CallDeadline.scala rename explore/src/main/scala/explore/proposal/{ProgramUsers.scala => InvestigatorUsers.scala} (72%) create mode 100644 explore/src/main/scala/explore/users/CreateInviteProcess.scala rename explore/src/main/scala/explore/{proposal => users}/InviteUserPopup.scala (99%) diff --git a/explore/src/main/scala/explore/proposal/CallDeadline.scala b/explore/src/main/scala/explore/proposal/CallDeadline.scala new file mode 100644 index 0000000000..239f06332a --- /dev/null +++ b/explore/src/main/scala/explore/proposal/CallDeadline.scala @@ -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 + ) + ) diff --git a/explore/src/main/scala/explore/proposal/ProgramUsers.scala b/explore/src/main/scala/explore/proposal/InvestigatorUsers.scala similarity index 72% rename from explore/src/main/scala/explore/proposal/ProgramUsers.scala rename to explore/src/main/scala/explore/proposal/InvestigatorUsers.scala index 0ec0cc57c1..0a5a1dcc8e 100644 --- a/explore/src/main/scala/explore/proposal/ProgramUsers.scala +++ b/explore/src/main/scala/explore/proposal/InvestigatorUsers.scala @@ -3,7 +3,6 @@ package explore.proposal -import cats.data.NonEmptyList import cats.data.NonEmptySet import cats.syntax.all.* import crystal.react.* @@ -12,6 +11,7 @@ import explore.components.Tile import explore.model.ProgramUserWithRole import explore.model.ProposalTabTileIds import explore.model.UserInvitation +import explore.users.CreateInviteProcess import explore.users.ProgramUserInvitations import explore.users.ProgramUsersTable import japgolly.scalajs.react.* @@ -19,7 +19,6 @@ import japgolly.scalajs.react.vdom.html_<^.* import lucuma.core.enums.InvitationStatus import lucuma.core.enums.ProgramUserRole import lucuma.core.model.Program -import lucuma.core.util.Enumerated import lucuma.core.util.NewType import lucuma.react.common.ReactFnProps import lucuma.react.primereact.Button @@ -27,25 +26,19 @@ import lucuma.react.primereact.OverlayPanelRef import lucuma.ui.primereact.* import lucuma.ui.syntax.all.given -enum CreateInviteProcess(private val tag: String) derives Enumerated: - case Idle extends CreateInviteProcess("idle") - case Running extends CreateInviteProcess("running") - case Error extends CreateInviteProcess("error") - case Done extends CreateInviteProcess("done") +case class InvestigatorUsers private ( + pid: Program.Id, + readOnly: Boolean, + users: View[List[ProgramUserWithRole]], + invitations: View[List[UserInvitation]], + private val state: View[InvestigatorUsers.ProgramUsersState] +) extends ReactFnProps(InvestigatorUsers.component) -object ProgramUsersState extends NewType[CreateInviteProcess] -type ProgramUsersState = ProgramUsersState.Type +object InvestigatorUsers: + private type Props = InvestigatorUsers -case class ProgramUsers( - pid: Program.Id, - readOnly: Boolean, - users: View[List[ProgramUserWithRole]], - invitations: View[List[UserInvitation]], - state: View[ProgramUsersState] -) extends ReactFnProps(ProgramUsers.component) - -object ProgramUsers: - private type Props = ProgramUsers + protected object ProgramUsersState extends NewType[CreateInviteProcess] + private type ProgramUsersState = ProgramUsersState.Type private def inviteControl( readOnly: Boolean, @@ -73,7 +66,9 @@ object ProgramUsers: ProposalTabTileIds.UsersId.id, "Investigators", ProgramUsersState(CreateInviteProcess.Idle) - )(ProgramUsers(pid, readOnly, users, invitations, _), (s, _) => inviteControl(readOnly, ref, s)) + )(InvestigatorUsers(pid, readOnly, users, invitations, _), + (s, _) => inviteControl(readOnly, ref, s) + ) private val component = ScalaFnComponent[Props]: props => diff --git a/explore/src/main/scala/explore/proposal/ProposalEditor.scala b/explore/src/main/scala/explore/proposal/ProposalEditor.scala index f3f9bb4e0a..55be73a3d2 100644 --- a/explore/src/main/scala/explore/proposal/ProposalEditor.scala +++ b/explore/src/main/scala/explore/proposal/ProposalEditor.scala @@ -26,6 +26,7 @@ import explore.model.UserInvitation import explore.model.enums.GridLayoutSection import explore.model.layout.LayoutsMap import explore.undo.* +import explore.users.InviteUserPopup import japgolly.scalajs.react.* import japgolly.scalajs.react.vdom.html_<^.* import lucuma.core.enums.ProgramUserRole @@ -130,7 +131,7 @@ object ProposalEditor: ) val usersTile = - ProgramUsers.programUsersTile( + InvestigatorUsers.programUsersTile( props.programId, props.readonly, props.users, diff --git a/explore/src/main/scala/explore/proposal/ProposalTabContents.scala b/explore/src/main/scala/explore/proposal/ProposalTabContents.scala index 7b32bff1b5..ef2cf4bf8b 100644 --- a/explore/src/main/scala/explore/proposal/ProposalTabContents.scala +++ b/explore/src/main/scala/explore/proposal/ProposalTabContents.scala @@ -26,9 +26,7 @@ import explore.model.layout.LayoutsMap import explore.syntax.ui.* import explore.undo.* import explore.utils.* -import fs2.Stream import japgolly.scalajs.react.* -import japgolly.scalajs.react.hooks.Hooks.UseState import japgolly.scalajs.react.vdom.html_<^.* import lucuma.core.enums.ProgramType import lucuma.core.model.Program @@ -54,37 +52,6 @@ import lucuma.ui.syntax.all.given import org.typelevel.log4cats.Logger import queries.common.ProposalQueriesGQL.* -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 - ) - ) - ) - } - case class ProposalTabContents( programId: Program.Id, userVault: Option[UserVault], @@ -97,8 +64,9 @@ case class ProposalTabContents( ) extends ReactFnProps(ProposalTabContents.component) object ProposalTabContents: + private type Props = ProposalTabContents + private object IsUpdatingStatus extends NewType[Boolean] - private type IsUpdatingStatus = IsUpdatingStatus.Type private def createProposal( programId: Program.Id, @@ -117,164 +85,132 @@ object ProposalTabContents: .void .runAsync - private def renderFn( - programId: Program.Id, - userVault: Option[UserVault], - programDetails: View[ProgramDetails], - cfps: List[CallForProposal], - timeEstimateRange: Pot[Option[ProgramTimeRange]], - attachments: View[List[ProposalAttachment]], - undoStacks: View[UndoStacks[IO, Proposal]], - ctx: AppContext[IO], - layout: LayoutsMap, - isUpdatingStatus: View[IsUpdatingStatus], - readonly: Boolean, - errorMessage: UseState[Option[String]] - ): VdomNode = { - import ctx.given - - val invitations = programDetails.zoom(ProgramDetails.invitations) - val users = programDetails.zoom(ProgramDetails.allUsers) - - val isStdUser = userVault.map(_.user).collect { case _: StandardUser => () }.isDefined - val proposalStatus = programDetails.get.proposalStatus - - def updateStatus(newStatus: ProposalStatus): Callback = - (for { - _ <- SetProposalStatus[IO] - .execute(SetProposalStatusInput(programId = programId.assign, status = newStatus)) - .onError { - case ResponseException(errors, _) => - errorMessage.setState(errors.head.message.some).to[IO] - case e => - errorMessage.setState(Some(e.getMessage.toString)).to[IO] - } - .void - _ <- programDetails.zoom(ProgramDetails.proposalStatus).set(newStatus).toAsync - } yield ()).switching(isUpdatingStatus.async, IsUpdatingStatus(_)).runAsync - - if (programDetails.get.programType =!= ProgramType.Science) - <.div(ExploreStyles.HVCenter, - Message(text = "Only Science Program Types can have proposals.", - severity = Message.Severity.Info - ) - ) - else - programDetails - .zoom(ProgramDetails.proposal) - .mapValue((proposalView: View[Proposal]) => - val piPartner = - programDetails.zoom(ProgramDetails.piPartner.some).get - - val deadline: Option[Timestamp] = - proposalView.get.deadline(cfps, piPartner) - - <.div( - ExploreStyles.ProposalTab, - ProposalEditor( - programId, - userVault.map(_.user.id), - proposalView, - undoStacks, - timeEstimateRange, - users, - invitations, - attachments, - userVault.map(_.token), - cfps, - layout, - readonly - ), - Toolbar(left = - <.div( - ExploreStyles.ProposalSubmissionBar, - Tag( - value = programDetails.get.proposalStatus.name, - severity = - if (proposalStatus === ProposalStatus.Accepted) Tag.Severity.Success - else Tag.Severity.Danger - ) - .when(proposalStatus > ProposalStatus.Submitted), - // TODO: Validate proposal before allowing submission - React - .Fragment( - Button(label = "Submit Proposal", - onClick = updateStatus(ProposalStatus.Submitted), - disabled = isUpdatingStatus.get.value || proposalView.get.callId.isEmpty - ).compact.tiny, - deadline.map(CallDeadline.apply) - ) - .when( - isStdUser && proposalStatus === ProposalStatus.NotSubmitted - ), - Button("Retract Proposal", - severity = Button.Severity.Warning, - onClick = updateStatus(ProposalStatus.NotSubmitted), - disabled = isUpdatingStatus.get.value - ).compact.tiny - .when( - isStdUser && proposalStatus === ProposalStatus.Submitted - ), - errorMessage.value.map(r => Message(text = r, severity = Message.Severity.Error)) - ) - ) - ) - ) - .getOrElse( - if (isStdUser) - <.div( - ExploreStyles.HVCenter, - Button( - label = "Create a Proposal", - icon = Icons.FileCirclePlus.withClass(LoginStyles.LoginOrcidIcon), - clazz = LoginStyles.LoginBoxButton, - severity = Button.Severity.Secondary, - onClick = createProposal( - programId, - programDetails - ) - ).big - ) - else - <.div( - ExploreStyles.HVCenter, - Button( - label = "Login with ORCID to create a Proposal", - icon = Image(src = Resources.OrcidLogo, clazz = LoginStyles.LoginOrcidIcon), - clazz = LoginStyles.LoginBoxButton, - severity = Button.Severity.Secondary, - onClick = ctx.sso.switchToORCID.runAsync - ).big - ) - ) - } - - private type Props = ProposalTabContents - private val component = ScalaFnComponent .withHooks[Props] .useContext(AppContext.ctx) .useStateView(IsUpdatingStatus(false)) - .useMemoBy((props, _, _) => props.programDetails.get.proposalStatus)((_, _, _) => + .useMemoBy((props, _, _) => props.programDetails.get.proposalStatus): (_, _, _) => p => p === ProposalStatus.Submitted || p === ProposalStatus.Accepted - ) .useState(none[String]) // Submission error message .useLayoutEffectWithDepsBy((props, _, _, _, _) => props.programDetails.get.proposal.flatMap(_.callId) )((_, _, _, _, e) => _ => e.setState(none)) - .render { (props, ctx, isUpdatingStatus, readonly, errorMsg) => - renderFn( - props.programId, - props.userVault, - props.programDetails, - props.cfps, - props.timeEstimateRange, - props.attachments, - props.undoStacks, - ctx, - props.layout, - isUpdatingStatus, - readonly, - errorMsg - ) - } + .render: (props, ctx, isUpdatingStatus, readonly, errorMessage) => + + import ctx.given + + val invitations = props.programDetails.zoom(ProgramDetails.invitations) + val users = props.programDetails.zoom(ProgramDetails.allUsers) + + val isStdUser = props.userVault.map(_.user).collect { case _: StandardUser => () }.isDefined + val proposalStatus = props.programDetails.get.proposalStatus + + def updateStatus(newStatus: ProposalStatus): Callback = + (for { + _ <- SetProposalStatus[IO] + .execute: + SetProposalStatusInput(programId = props.programId.assign, status = newStatus) + .onError: + case ResponseException(errors, _) => + errorMessage.setState(errors.head.message.some).to[IO] + case e => + errorMessage.setState(Some(e.getMessage.toString)).to[IO] + .void + _ <- props.programDetails.zoom(ProgramDetails.proposalStatus).set(newStatus).toAsync + } yield ()).switching(isUpdatingStatus.async, IsUpdatingStatus(_)).runAsync + + if (props.programDetails.get.programType =!= ProgramType.Science) + <.div(ExploreStyles.HVCenter)( + Message( + text = "Only Science Program Types can have proposals.", + severity = Message.Severity.Info + ) + ) + else + props.programDetails + .zoom(ProgramDetails.proposal) + .mapValue((proposalView: View[Proposal]) => + val piPartner = + props.programDetails.zoom(ProgramDetails.piPartner.some).get + + val deadline: Option[Timestamp] = + proposalView.get.deadline(props.cfps, piPartner) + + <.div( + ExploreStyles.ProposalTab, + ProposalEditor( + props.programId, + props.userVault.map(_.user.id), + proposalView, + props.undoStacks, + props.timeEstimateRange, + users, + invitations, + props.attachments, + props.userVault.map(_.token), + props.cfps, + props.layout, + readonly + ), + Toolbar(left = + <.div( + ExploreStyles.ProposalSubmissionBar, + Tag( + value = props.programDetails.get.proposalStatus.name, + severity = + if (proposalStatus === ProposalStatus.Accepted) Tag.Severity.Success + else Tag.Severity.Danger + ) + .when(proposalStatus > ProposalStatus.Submitted), + // TODO: Validate proposal before allowing submission + React + .Fragment( + Button( + label = "Submit Proposal", + onClick = updateStatus(ProposalStatus.Submitted), + disabled = isUpdatingStatus.get.value || proposalView.get.callId.isEmpty + ).compact.tiny, + deadline.map(CallDeadline.apply) + ) + .when: + isStdUser && proposalStatus === ProposalStatus.NotSubmitted + , + Button( + "Retract Proposal", + severity = Button.Severity.Warning, + onClick = updateStatus(ProposalStatus.NotSubmitted), + disabled = isUpdatingStatus.get.value + ).compact.tiny + .when: + isStdUser && proposalStatus === ProposalStatus.Submitted + , + errorMessage.value.map(r => Message(text = r, severity = Message.Severity.Error)) + ) + ) + ) + ) + .getOrElse( + if (isStdUser) + <.div(ExploreStyles.HVCenter)( + Button( + label = "Create a Proposal", + icon = Icons.FileCirclePlus.withClass(LoginStyles.LoginOrcidIcon), + clazz = LoginStyles.LoginBoxButton, + severity = Button.Severity.Secondary, + onClick = createProposal( + props.programId, + props.programDetails + ) + ).big + ) + else + <.div(ExploreStyles.HVCenter)( + Button( + label = "Login with ORCID to create a Proposal", + icon = Image(src = Resources.OrcidLogo, clazz = LoginStyles.LoginOrcidIcon), + clazz = LoginStyles.LoginBoxButton, + severity = Button.Severity.Secondary, + onClick = ctx.sso.switchToORCID.runAsync + ).big + ) + ) diff --git a/explore/src/main/scala/explore/users/CreateInviteProcess.scala b/explore/src/main/scala/explore/users/CreateInviteProcess.scala new file mode 100644 index 0000000000..9ae41333ea --- /dev/null +++ b/explore/src/main/scala/explore/users/CreateInviteProcess.scala @@ -0,0 +1,12 @@ +// 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.users + +import lucuma.core.util.Enumerated + +enum CreateInviteProcess(private val tag: String) derives Enumerated: + case Idle extends CreateInviteProcess("idle") + case Running extends CreateInviteProcess("running") + case Error extends CreateInviteProcess("error") + case Done extends CreateInviteProcess("done") diff --git a/explore/src/main/scala/explore/proposal/InviteUserPopup.scala b/explore/src/main/scala/explore/users/InviteUserPopup.scala similarity index 99% rename from explore/src/main/scala/explore/proposal/InviteUserPopup.scala rename to explore/src/main/scala/explore/users/InviteUserPopup.scala index 8dced2e08c..50f5f635ea 100644 --- a/explore/src/main/scala/explore/proposal/InviteUserPopup.scala +++ b/explore/src/main/scala/explore/users/InviteUserPopup.scala @@ -1,7 +1,7 @@ // 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 +package explore.users import cats.effect.IO import cats.syntax.all.* diff --git a/explore/src/main/scala/explore/users/ProgramUserInvitations.scala b/explore/src/main/scala/explore/users/ProgramUserInvitations.scala index b96ad868e0..22e18548a6 100644 --- a/explore/src/main/scala/explore/users/ProgramUserInvitations.scala +++ b/explore/src/main/scala/explore/users/ProgramUserInvitations.scala @@ -29,7 +29,7 @@ import lucuma.ui.syntax.all.given import lucuma.ui.table.* import queries.common.InvitationQueriesGQL.* -case class ProgramUserInvitations(invitations: View[List[UserInvitation]], readOnly: Boolean) +case class ProgramUserInvitations(invitations: View[List[UserInvitation]], readonly: Boolean) extends ReactFnProps(ProgramUserInvitations.component) object ProgramUserInvitations: @@ -132,7 +132,7 @@ object ProgramUserInvitations: meta = TableMeta( isActive = isActive, invitations = props.invitations, - readOnly = props.readOnly + readOnly = props.readonly ) ) .render: (props, _, _, _, _, table) => From 11ddc495dc886157ba409cc5327298de9c9190c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ra=C3=BAl=20Piaggio?= Date: Wed, 18 Sep 2024 20:57:58 -0300 Subject: [PATCH 07/11] support users tables --- explore/src/main/scala/explore/Routing.scala | 11 ++- .../explore/programs/ProgramDetailsTile.scala | 56 ++++++++---- .../scala/explore/programs/SupportUsers.scala | 35 ++++++-- .../explore/tabs/ProgramTabContents.scala | 81 +++++++++-------- .../explore/users/ProgramUsersTable.scala | 87 +++++++++---------- .../explore/model/ProgramUserWithRole.scala | 2 - .../main/scala/explore/model/display.scala | 7 ++ 7 files changed, 168 insertions(+), 111 deletions(-) diff --git a/explore/src/main/scala/explore/Routing.scala b/explore/src/main/scala/explore/Routing.scala index 2b6d337e44..85c263e17a 100644 --- a/explore/src/main/scala/explore/Routing.scala +++ b/explore/src/main/scala/explore/Routing.scala @@ -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, @@ -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) diff --git a/explore/src/main/scala/explore/programs/ProgramDetailsTile.scala b/explore/src/main/scala/explore/programs/ProgramDetailsTile.scala index 65df687d79..fe2428685c 100644 --- a/explore/src/main/scala/explore/programs/ProgramDetailsTile.scala +++ b/explore/src/main/scala/explore/programs/ProgramDetailsTile.scala @@ -5,33 +5,42 @@ 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.UserInvitation import japgolly.scalajs.react.* import japgolly.scalajs.react.vdom.html_<^.* -import lucuma.core.enums.ProgramUserRole +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 supportPrimary: List[ProgramUserWithRole] = - details.allUsers.filter(_.role === ProgramUserRole.SupportPrimary) + 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)( @@ -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(supportPrimary.map(_.nameWithEmail).mkString(", "), "Contact Scientists") - .when(supportPrimary.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") ) ) diff --git a/explore/src/main/scala/explore/programs/SupportUsers.scala b/explore/src/main/scala/explore/programs/SupportUsers.scala index 24a57fb885..ee23642d85 100644 --- a/explore/src/main/scala/explore/programs/SupportUsers.scala +++ b/explore/src/main/scala/explore/programs/SupportUsers.scala @@ -6,6 +6,7 @@ package explore.programs import cats.data.NonEmptySet import crystal.react.View import explore.model.ProgramUserWithRole +import explore.model.UserInvitation import explore.users.ProgramUsersTable import japgolly.scalajs.react.* import japgolly.scalajs.react.vdom.html_<^.* @@ -14,18 +15,36 @@ import lucuma.core.model.Program import lucuma.react.common.ReactFnProps case class SupportUsers( - programId: Program.Id, - users: View[List[ProgramUserWithRole]], - readonly: Boolean + 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 => - ProgramUsersTable( - props.programId, - props.users, - NonEmptySet.of(ProgramUserRole.SupportPrimary, ProgramUserRole.SupportSecondary), - props.readonly + <.div( + <.label(props.title, "+ Add"), + ProgramUsersTable( + props.programId, + props.users, + 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 + ) + ) ) diff --git a/explore/src/main/scala/explore/tabs/ProgramTabContents.scala b/explore/src/main/scala/explore/tabs/ProgramTabContents.scala index ebd65d66e7..8e65fdb0c7 100644 --- a/explore/src/main/scala/explore/tabs/ProgramTabContents.scala +++ b/explore/src/main/scala/explore/tabs/ProgramTabContents.scala @@ -4,7 +4,9 @@ package explore.tabs import cats.effect.IO +import cats.syntax.option.* import crystal.Pot +import crystal.react.View import explore.* import explore.components.Tile import explore.components.TileController @@ -22,6 +24,7 @@ import explore.programs.ProgramDetailsTile import explore.programs.ProgramNotesTile import japgolly.scalajs.react.* import japgolly.scalajs.react.vdom.html_<^.* +import lucuma.core.model.Program import lucuma.core.model.Semester import lucuma.react.common.ReactFnProps import lucuma.react.resizeDetector.* @@ -30,7 +33,8 @@ import lucuma.ui.sso.UserVault import lucuma.ui.syntax.all.given case class ProgramTabContents( - programDetails: ProgramDetails, + programId: Program.Id, + programDetails: View[ProgramDetails], userVault: Option[UserVault], programTimes: Pot[ProgramTimes], semester: Semester, @@ -45,41 +49,50 @@ object ProgramTabContents: .useContext(AppContext.ctx) .useResizeDetector() .render: (props, _, resize) => - val defaultLayouts: LayoutsMap = - ExploreGridLayouts.sectionLayout(GridLayoutSection.ProgramsLayout) + props.userVault.map: userVault => + val defaultLayouts: LayoutsMap = + ExploreGridLayouts.sectionLayout(GridLayoutSection.ProgramsLayout) - val layouts: LayoutsMap = - props.userPreferences.programsTabLayout + val layouts: LayoutsMap = + props.userPreferences.programsTabLayout - val detailsTile = - Tile( - ProgramTabTileIds.DetailsId.id, - "Program Details" - )(_ => ProgramDetailsTile(props.programDetails, props.programTimes, props.semester)) + val detailsTile = + Tile( + ProgramTabTileIds.DetailsId.id, + "Program Details" + )(_ => + ProgramDetailsTile( + props.programId, + props.programDetails, + props.programTimes, + props.semester, + userVault.user.role.access + ) + ) - val notesTile = - Tile( - ProgramTabTileIds.NotesId.id, - "Notes" - )(_ => ProgramNotesTile()) + val notesTile = + Tile( + ProgramTabTileIds.NotesId.id, + "Notes" + )(_ => ProgramNotesTile()) - val changeRequestsTile = - Tile( - ProgramTabTileIds.ChangeRequestsId.id, - "Change Requests" - )(_ => ProgramChangeRequestsTile()) + val changeRequestsTile = + Tile( + ProgramTabTileIds.ChangeRequestsId.id, + "Change Requests" + )(_ => ProgramChangeRequestsTile()) - <.div(ExploreStyles.MultiPanelTile)( - TileController( - props.userVault.map(_.user.id), - resize.width.getOrElse(1), - defaultLayouts, - layouts, - List( - detailsTile, - notesTile, - changeRequestsTile - ), - GridLayoutSection.ProgramsLayout - ) - ).withRef(resize.ref) + <.div(ExploreStyles.MultiPanelTile)( + TileController( + userVault.user.id.some, + resize.width.getOrElse(1), + defaultLayouts, + layouts, + List( + detailsTile, + notesTile, + changeRequestsTile + ), + GridLayoutSection.ProgramsLayout + ) + ).withRef(resize.ref) diff --git a/explore/src/main/scala/explore/users/ProgramUsersTable.scala b/explore/src/main/scala/explore/users/ProgramUsersTable.scala index 6bb21dec45..9450cad89f 100644 --- a/explore/src/main/scala/explore/users/ProgramUsersTable.scala +++ b/explore/src/main/scala/explore/users/ProgramUsersTable.scala @@ -46,10 +46,11 @@ import monocle.function.Each.* import queries.common.ProposalQueriesGQL.UnlinkUser case class ProgramUsersTable( - programId: Program.Id, - users: View[List[ProgramUserWithRole]], - filterRoles: NonEmptySet[ProgramUserRole], - readOnly: Boolean + programId: Program.Id, + users: View[List[ProgramUserWithRole]], + filterRoles: NonEmptySet[ProgramUserRole], + readonly: Boolean, + hiddenColumns: Set[ProgramUsersTable.Column] = Set.empty ) extends ReactFnProps(ProgramUsersTable.component) object ProgramUsersTable: @@ -64,33 +65,27 @@ object ProgramUsersTable: private val ColDef = ColumnDef.WithTableMeta[View[ProgramUserWithRole], TableMeta] - private val NameColumnId: ColumnId = ColumnId("name") - private val PartnerColumnId: ColumnId = ColumnId("Partner") - private val EmailColumnId: ColumnId = ColumnId("email") - private val ESColumnId: ColumnId = ColumnId("education") - private val ThesisColumnId: ColumnId = ColumnId("thesis") - private val GenderColumnId: ColumnId = ColumnId("gender") - private val OrcidIdColumnId: ColumnId = ColumnId("orcid-id") - private val RoleColumnId: ColumnId = ColumnId("role") - private val UnlinkId: ColumnId = ColumnId("unlink") + enum Column( + protected[ProgramUsersTable] val tag: String, + protected[ProgramUsersTable] val header: String + ) derives Enumerated: + val id: ColumnId = ColumnId(tag) - private val columnNames: Map[ColumnId, String] = Map( - NameColumnId -> "Name", - PartnerColumnId -> "Partner", - EmailColumnId -> "email", - ESColumnId -> "education", - ThesisColumnId -> "thesis", - GenderColumnId -> "gender", - OrcidIdColumnId -> "ORCID", - RoleColumnId -> "Role", - UnlinkId -> "" - ) + case Name extends Column("name", "Name") + case Partner extends Column("partner", "Partner") + case Email extends Column("email", "Email") + case EducationalStatus extends Column("education", "Education") + case Thesis extends Column("thesis", "Thesis") + case Gender extends Column("gender", "Gender") + case OrcidId extends Column("orcid-id", "ORCID") + case Role extends Column("role", "Role") + case Unlink extends Column("unlink", "") private def column[V]( - id: ColumnId, + column: Column, accessor: View[ProgramUserWithRole] => V ): ColumnDef.Single.WithTableMeta[View[ProgramUserWithRole], V, TableMeta] = - ColDef(id, accessor, columnNames(id)) + ColDef(column.id, accessor, column.header) val partnerLinkOptions: List[PartnerLink] = PartnerLink.HasNonPartner :: Enumerated[Partner].all.map { p => @@ -146,9 +141,9 @@ object ProgramUsersTable: import ctx.given List( - column(NameColumnId, _.get.name), + column(Column.Name, _.get.name), ColDef( - PartnerColumnId, + Column.Partner.id, _.zoom(ProgramUserWithRole.partnerLink), enableSorting = true, enableResizing = true, @@ -166,14 +161,13 @@ object ProgramUsersTable: partnerSelector(pl, usersView.set, meta.readOnly || meta.isActive.get.value) ), - column(EmailColumnId, _.get.user.profile.foldMap(_.primaryEmail).getOrElse("-")), + column(Column.Email, _.get.user.profile.foldMap(_.primaryEmail).getOrElse("-")), ColDef( - ESColumnId, + Column.EducationalStatus.id, _.zoom(ProgramUserWithRole.educationalStatus), enableSorting = true, enableResizing = true, cell = c => - val cell = c.row.original val userId = cell.get.user.id c.table.options.meta.map: meta => @@ -193,10 +187,9 @@ object ProgramUsersTable: ) ), ColDef( - ThesisColumnId, + Column.Thesis.id, _.zoom(ProgramUserWithRole.thesis), cell = c => - val cell = c.row.original val userId = cell.get.user.id @@ -210,15 +203,13 @@ object ProgramUsersTable: ) ), ColDef( - GenderColumnId, + Column.Gender.id, _.zoom(ProgramUserWithRole.gender), cell = c => - val cell = c.row.original val userId = cell.get.user.id c.table.options.meta.map: meta => - val view = c.value .withOnMod(th => updateUserGender[IO](meta.programId, userId, th).runAsyncAndForget) EnumOptionalDropdown[Gender]( @@ -233,18 +224,17 @@ object ProgramUsersTable: onChange = view.set ) ), - column(OrcidIdColumnId, _.get.user.profile.foldMap(_.orcidId.value)), - column(RoleColumnId, _.get.roleName), + column(Column.OrcidId, _.get.user.profile.foldMap(_.orcidId.value)), + column(Column.Role, _.get.role.shortName), ColDef( - UnlinkId, - identity, + Column.Unlink.id, + _.get, "", enableSorting = false, enableResizing = false, cell = cell => cell.table.options.meta.map: meta => - - val userId = cell.value.get.user.id + val userId = cell.value.user.id val action = UnlinkUser[IO].execute(UnlinkUserInput(meta.programId, userId)) *> meta.users.mod(_.filterNot(_.user.id === userId)).to[IO] @@ -264,7 +254,7 @@ object ProgramUsersTable: disabled = meta.readOnly || meta.isActive.get.value, onClick = unlink ).mini.compact - .unless(cell.value.get.role === ProgramUserRole.Pi) // don't allow removing the PI + .unless(cell.value.role === ProgramUserRole.Pi) // don't allow removing the PI ) , size = 35.toPx @@ -285,11 +275,18 @@ object ProgramUsersTable: cols, rows, getRowId = (row, _, _) => RowId(row.get.user.id.toString), - meta = TableMeta(props.programId, props.users, props.readOnly, isActive) + meta = TableMeta(props.programId, props.users, props.readonly, isActive), + state = PartialTableState( + columnVisibility = ColumnVisibility( + (props.hiddenColumns.map(_.id -> Visibility.Hidden) + + (Column.Unlink.id -> Visibility.fromVisible(!props.readonly))).toMap + ) + ) ) .render: (props, _, _, _, _, table) => PrimeTable( table, striped = true, - compact = Compact.Very + compact = Compact.Very, + emptyMessage = "No users defined" ) diff --git a/model/shared/src/main/scala/explore/model/ProgramUserWithRole.scala b/model/shared/src/main/scala/explore/model/ProgramUserWithRole.scala index 68da90884f..943effa10b 100644 --- a/model/shared/src/main/scala/explore/model/ProgramUserWithRole.scala +++ b/model/shared/src/main/scala/explore/model/ProgramUserWithRole.scala @@ -25,8 +25,6 @@ case class ProgramUserWithRole( ) derives Eq: export user.{name, nameWithEmail} - lazy val roleName: String = role.tag - object ProgramUserWithRole: val user: Lens[ProgramUserWithRole, ProgramUser] = Focus[ProgramUserWithRole](_.user) diff --git a/model/shared/src/main/scala/explore/model/display.scala b/model/shared/src/main/scala/explore/model/display.scala index e44b6ff643..c9efce799c 100644 --- a/model/shared/src/main/scala/explore/model/display.scala +++ b/model/shared/src/main/scala/explore/model/display.scala @@ -211,6 +211,13 @@ trait DisplayImplicits: case Gender.Other => "Other" case Gender.NotSpecified => "Not Specified" + given Display[ProgramUserRole] = Display.byShortName: + case ProgramUserRole.Pi => "PI" + case ProgramUserRole.Coi => "CoI" + case ProgramUserRole.CoiRO => "Observer" + case ProgramUserRole.SupportPrimary => "Principal Support" + case ProgramUserRole.SupportSecondary => "Additional Support" + extension (configuration: BasicConfiguration) def configurationSummary: String = configuration match case BasicConfiguration.GmosNorthLongSlit(grating, _, fpu, _) => From 53c09669b27d5561f252007787213a58d07c1346 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ra=C3=BAl=20Piaggio?= Date: Wed, 18 Sep 2024 22:36:45 -0300 Subject: [PATCH 08/11] accept invitations for support users --- .../scala/explore/programs/SupportUsers.scala | 7 +- .../explore/proposal/InvestigatorUsers.scala | 70 ++++--------------- .../explore/proposal/ProposalEditor.scala | 38 +++++++--- .../explore/users/CreateInviteProcess.scala | 12 ---- .../explore/users/CreateInviteStatus.scala | 12 ++++ .../explore/users/InviteUserButton.scala | 53 ++++++++++++++ .../scala/explore/users/InviteUserPopup.scala | 61 +++++++++------- .../users/ProgramUserInvitations.scala | 24 ++++--- .../explore/users/ProgramUsersTable.scala | 29 ++++++-- 9 files changed, 183 insertions(+), 123 deletions(-) delete mode 100644 explore/src/main/scala/explore/users/CreateInviteProcess.scala create mode 100644 explore/src/main/scala/explore/users/CreateInviteStatus.scala create mode 100644 explore/src/main/scala/explore/users/InviteUserButton.scala diff --git a/explore/src/main/scala/explore/programs/SupportUsers.scala b/explore/src/main/scala/explore/programs/SupportUsers.scala index ee23642d85..98e8b0024b 100644 --- a/explore/src/main/scala/explore/programs/SupportUsers.scala +++ b/explore/src/main/scala/explore/programs/SupportUsers.scala @@ -13,6 +13,7 @@ import japgolly.scalajs.react.vdom.html_<^.* import lucuma.core.enums.ProgramUserRole import lucuma.core.model.Program import lucuma.react.common.ReactFnProps +import explore.users.InviteUserButton case class SupportUsers( programId: Program.Id, @@ -32,10 +33,14 @@ object SupportUsers: private val component = ScalaFnComponent[Props]: props => <.div( - <.label(props.title, "+ Add"), + <.label( + props.title, + InviteUserButton(props.programId, props.supportRole.role, props.invitations) + ), ProgramUsersTable( props.programId, props.users, + props.invitations, NonEmptySet.one(props.supportRole.role), props.readonly, hiddenColumns = Set( diff --git a/explore/src/main/scala/explore/proposal/InvestigatorUsers.scala b/explore/src/main/scala/explore/proposal/InvestigatorUsers.scala index 0a5a1dcc8e..79a4080098 100644 --- a/explore/src/main/scala/explore/proposal/InvestigatorUsers.scala +++ b/explore/src/main/scala/explore/proposal/InvestigatorUsers.scala @@ -4,85 +4,39 @@ package explore.proposal import cats.data.NonEmptySet -import cats.syntax.all.* import crystal.react.* -import explore.Icons -import explore.components.Tile import explore.model.ProgramUserWithRole -import explore.model.ProposalTabTileIds import explore.model.UserInvitation -import explore.users.CreateInviteProcess -import explore.users.ProgramUserInvitations +import explore.users.CreateInviteStatus import explore.users.ProgramUsersTable import japgolly.scalajs.react.* import japgolly.scalajs.react.vdom.html_<^.* -import lucuma.core.enums.InvitationStatus import lucuma.core.enums.ProgramUserRole import lucuma.core.model.Program import lucuma.core.util.NewType import lucuma.react.common.ReactFnProps -import lucuma.react.primereact.Button -import lucuma.react.primereact.OverlayPanelRef -import lucuma.ui.primereact.* import lucuma.ui.syntax.all.given -case class InvestigatorUsers private ( +case class InvestigatorUsers( pid: Program.Id, - readOnly: Boolean, + readonly: Boolean, users: View[List[ProgramUserWithRole]], invitations: View[List[UserInvitation]], - private val state: View[InvestigatorUsers.ProgramUsersState] + private val state: View[InvestigatorUsers.State] ) extends ReactFnProps(InvestigatorUsers.component) object InvestigatorUsers: private type Props = InvestigatorUsers - protected object ProgramUsersState extends NewType[CreateInviteProcess] - private type ProgramUsersState = ProgramUsersState.Type - - private def inviteControl( - readOnly: Boolean, - ref: OverlayPanelRef, - state: View[ProgramUsersState] - ) = - Button( - severity = Button.Severity.Secondary, - size = Button.Size.Small, - disabled = readOnly, - loading = state.get.value == CreateInviteProcess.Running, - icon = Icons.UserPlus, - tooltip = "Create CoI invitation", - onClickE = ref.toggle - ).tiny.compact - - def programUsersTile( - pid: Program.Id, - readOnly: Boolean, - users: View[List[ProgramUserWithRole]], - invitations: View[List[UserInvitation]], - ref: OverlayPanelRef - ) = - Tile( - ProposalTabTileIds.UsersId.id, - "Investigators", - ProgramUsersState(CreateInviteProcess.Idle) - )(InvestigatorUsers(pid, readOnly, users, invitations, _), - (s, _) => inviteControl(readOnly, ref, s) - ) + object State extends NewType[CreateInviteStatus] + type State = State.Type private val component = ScalaFnComponent[Props]: props => - <.div( - ProgramUsersTable( - props.pid, - props.users, - NonEmptySet.of(ProgramUserRole.Pi, ProgramUserRole.Coi, ProgramUserRole.CoiRO), - props.readOnly - ), - React - .Fragment( - "Pending invitations", - ProgramUserInvitations(props.invitations, props.readOnly) - ) - .when(props.invitations.get.filter(_.status === InvitationStatus.Pending).nonEmpty) + ProgramUsersTable( + props.pid, + props.users, + props.invitations, + NonEmptySet.of(ProgramUserRole.Pi, ProgramUserRole.Coi, ProgramUserRole.CoiRO), + props.readonly ) diff --git a/explore/src/main/scala/explore/proposal/ProposalEditor.scala b/explore/src/main/scala/explore/proposal/ProposalEditor.scala index 55be73a3d2..4f052d8a64 100644 --- a/explore/src/main/scala/explore/proposal/ProposalEditor.scala +++ b/explore/src/main/scala/explore/proposal/ProposalEditor.scala @@ -3,6 +3,7 @@ package explore.proposal +import cats.syntax.option.* import cats.effect.IO import clue.* import clue.data.Input @@ -26,14 +27,12 @@ import explore.model.UserInvitation import explore.model.enums.GridLayoutSection import explore.model.layout.LayoutsMap import explore.undo.* -import explore.users.InviteUserPopup import japgolly.scalajs.react.* import japgolly.scalajs.react.vdom.html_<^.* import lucuma.core.enums.ProgramUserRole import lucuma.core.model.Program import lucuma.core.model.User import lucuma.react.common.ReactFnProps -import lucuma.react.primereact.hooks.UseOverlayPanelRef.implicits.* import lucuma.react.resizeDetector.hooks.* import lucuma.refined.* import lucuma.schemas.ObservationDB @@ -44,6 +43,10 @@ import lucuma.ui.primereact.given import lucuma.ui.syntax.all.given import monocle.Iso import queries.common.ProposalQueriesGQL +import explore.users.InviteUserButton +import lucuma.ui.react.given +import explore.users.ProgramUsersTable +import cats.data.NonEmptySet case class ProposalEditor( programId: Program.Id, @@ -94,8 +97,7 @@ object ProposalEditor: // } // ) .useResizeDetector() - .useOverlayPanelRef - .render: (props, ctx, resize, overlayRef) => + .render: (props, ctx, resize) => import ctx.given val undoCtx: UndoContext[Proposal] = UndoContext(props.undoStacks, props.proposal) @@ -131,12 +133,27 @@ object ProposalEditor: ) val usersTile = - InvestigatorUsers.programUsersTile( - props.programId, - props.readonly, - props.users, - props.invitations, - overlayRef + Tile( + ProposalTabTileIds.UsersId.id, + "Investigators" + )( + _ => + ProgramUsersTable( + props.programId, + props.users, + props.invitations, + NonEmptySet.of(ProgramUserRole.Pi, ProgramUserRole.Coi, ProgramUserRole.CoiRO), + props.readonly + ), + (_, _) => + Option + .unless[VdomNode](props.readonly): + InviteUserButton( + props.programId, + ProgramUserRole.Coi, + props.invitations + ) + .orEmpty ) val abstractTile = @@ -159,7 +176,6 @@ object ProposalEditor: ) <.div(ExploreStyles.MultiPanelTile)( - InviteUserPopup(props.programId, ProgramUserRole.Coi, props.invitations, overlayRef), TileController( props.optUserId, resize.width.getOrElse(1), diff --git a/explore/src/main/scala/explore/users/CreateInviteProcess.scala b/explore/src/main/scala/explore/users/CreateInviteProcess.scala deleted file mode 100644 index 9ae41333ea..0000000000 --- a/explore/src/main/scala/explore/users/CreateInviteProcess.scala +++ /dev/null @@ -1,12 +0,0 @@ -// 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.users - -import lucuma.core.util.Enumerated - -enum CreateInviteProcess(private val tag: String) derives Enumerated: - case Idle extends CreateInviteProcess("idle") - case Running extends CreateInviteProcess("running") - case Error extends CreateInviteProcess("error") - case Done extends CreateInviteProcess("done") diff --git a/explore/src/main/scala/explore/users/CreateInviteStatus.scala b/explore/src/main/scala/explore/users/CreateInviteStatus.scala new file mode 100644 index 0000000000..07cecef02f --- /dev/null +++ b/explore/src/main/scala/explore/users/CreateInviteStatus.scala @@ -0,0 +1,12 @@ +// 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.users + +import lucuma.core.util.Enumerated + +enum CreateInviteStatus(private val tag: String) derives Enumerated: + case Idle extends CreateInviteStatus("idle") + case Running extends CreateInviteStatus("running") + case Error extends CreateInviteStatus("error") + case Done extends CreateInviteStatus("done") diff --git a/explore/src/main/scala/explore/users/InviteUserButton.scala b/explore/src/main/scala/explore/users/InviteUserButton.scala new file mode 100644 index 0000000000..a47e78bbab --- /dev/null +++ b/explore/src/main/scala/explore/users/InviteUserButton.scala @@ -0,0 +1,53 @@ +// 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.users + +import crystal.react.View +import japgolly.scalajs.react.* +import japgolly.scalajs.react.vdom.html_<^.* +import lucuma.react.common.ReactFnProps +import lucuma.react.primereact.Button +import lucuma.ui.primereact.* +import lucuma.ui.syntax.all.given +import explore.Icons +import lucuma.react.primereact.hooks.all.* +import lucuma.core.enums.ProgramUserRole +import explore.model.display.given +import lucuma.core.syntax.display.* +import lucuma.core.model.Program +import explore.model.UserInvitation +import crystal.react.hooks.* + +case class InviteUserButton( + programId: Program.Id, + role: ProgramUserRole, + invitations: View[List[UserInvitation]] +) extends ReactFnProps(InviteUserButton.component) + +object InviteUserButton: + private type Props = InviteUserButton + + private val component = + ScalaFnComponent + .withHooks[Props] + .useOverlayPanelRef + .useStateView(CreateInviteStatus.Idle) + .render: (props, overlayRef, createInviteStatus) => + React.Fragment( + InviteUserPopup( + props.programId, + ProgramUserRole.Coi, + props.invitations, + createInviteStatus, + overlayRef + ), + Button( + severity = Button.Severity.Secondary, + size = Button.Size.Small, + loading = createInviteStatus.get == CreateInviteStatus.Running, + icon = Icons.UserPlus, + tooltip = s"Create ${props.role.shortName} invitation", + onClickE = overlayRef.toggle + ).tiny.compact + ) diff --git a/explore/src/main/scala/explore/users/InviteUserPopup.scala b/explore/src/main/scala/explore/users/InviteUserPopup.scala index 50f5f635ea..bf0364b579 100644 --- a/explore/src/main/scala/explore/users/InviteUserPopup.scala +++ b/explore/src/main/scala/explore/users/InviteUserPopup.scala @@ -38,12 +38,15 @@ import lucuma.ui.syntax.all.given import org.typelevel.log4cats.Logger import queries.common.InvitationQueriesGQL.* import queries.common.InvitationQueriesGQL.CreateInviteMutation.Data +import lucuma.core.syntax.display.* +import explore.model.display.given case class InviteUserPopup( - pid: Program.Id, - role: ProgramUserRole, - invitations: View[List[UserInvitation]], - ref: OverlayPanelRef + programId: Program.Id, + role: ProgramUserRole, + invitations: View[List[UserInvitation]], + createInviteStatus: View[CreateInviteStatus], + overlayRef: OverlayPanelRef ) extends ReactFnProps(InviteUserPopup.component) object InviteUserPopup: @@ -57,50 +60,53 @@ object InviteUserPopup: ScalaFnComponent .withHooks[Props] .useContext(AppContext.ctx) - .useStateView(CreateInviteProcess.Idle) .useStateView(none[EmailAddress]) .useState(false) .useStateView(none[String]) - .render: (props, ctx, inviteState, emailView, validEmail, key) => + .render: (props, ctx, emailView, validEmail, key) => import ctx.given + val createInviteStatus: View[CreateInviteStatus] = props.createInviteStatus + def createInvitation( - createInvite: View[CreateInviteProcess], - pid: Program.Id, - email: EmailAddress, - viewKey: View[Option[String]] + email: EmailAddress, + viewKey: View[Option[String]] ): IO[Unit] = - (createInvite.set(CreateInviteProcess.Running).to[IO] *> - CreateInviteMutation[IO].execute(pid, email.value.value, props.role)).attempt + (props.createInviteStatus.set(CreateInviteStatus.Running).to[IO] *> + CreateInviteMutation[IO].execute( + props.programId, + email.value.value, + props.role + )).attempt .flatMap: case Left(e) => Logger[IO].error(e)("Error creating invitation") *> - createInvite.set(CreateInviteProcess.Error).to[IO] + createInviteStatus.set(CreateInviteStatus.Error).to[IO] case Right(r) => props.invitations.mod(r.createUserInvitation.invitation :: _).to[IO] *> viewKey.set(r.createUserInvitation.key.some).to[IO] *> - createInvite.set(CreateInviteProcess.Done).to[IO] + createInviteStatus.set(CreateInviteStatus.Done).to[IO] OverlayPanel( closeOnEscape = true, onHide = key.set(None) >> emailView.set(None).runAsyncAndForget )( <.div(PrimeStyles.Dialog)( - <.div(PrimeStyles.DialogHeader, "Create CoI invitation"), + <.div(PrimeStyles.DialogHeader, s"Create ${props.role.shortName} invitation"), <.div(PrimeStyles.DialogContent)( <.div(LucumaPrimeStyles.FormColumnCompact)( FormInputTextView( id = "email-invite".refined, value = emailView, label = "Email", - disabled = inviteState.get === CreateInviteProcess.Running, + disabled = createInviteStatus.get === CreateInviteStatus.Running, validFormat = MailValidator.optional, onValidChange = v => validEmail.setState(v) )(^.autoComplete := "off") ), <.div(LucumaPrimeStyles.FormColumn)( <.label( - "An invitation email has been sent. If you wish to send the invitation another way, copy and send the key below to your CoI, it won't be displayed again." + "An invitation email has been sent. If you wish to send the invitation another way, copy and send the key below to the invited user, it won't be displayed again." ) ).when(key.when(_.isDefined)), key.get.map(key => @@ -113,24 +119,27 @@ object InviteUserPopup: Message( text = "Error submitting user invite, try later", severity = Message.Severity.Error - ).when(inviteState.get === CreateInviteProcess.Error), + ).when(createInviteStatus.get === CreateInviteStatus.Error), Button( icon = Icons.Close, - onClickE = e => inviteState.set(CreateInviteProcess.Idle) *> props.ref.toggle(e), + onClickE = e => + createInviteStatus.set(CreateInviteStatus.Idle) *> props.overlayRef.toggle(e), label = "Close" - ).compact.when(inviteState.get === CreateInviteProcess.Done), + ).compact.when(createInviteStatus.get === CreateInviteStatus.Done), Button( icon = Icons.PaperPlaneTop, - loading = inviteState.get === CreateInviteProcess.Running, - disabled = !validEmail.value || inviteState.when(_ === CreateInviteProcess.Done), - onClick = inviteState.set(CreateInviteProcess.Idle) *> + loading = createInviteStatus.get === CreateInviteStatus.Running, + disabled = + !validEmail.value || createInviteStatus.when(_ === CreateInviteStatus.Done), + onClick = createInviteStatus.set(CreateInviteStatus.Idle) *> emailView.get - .map(e => createInvitation(inviteState, props.pid, e, key).runAsync) + .map: email => + createInvitation(email, key).runAsync .getOrEmpty, tooltip = "Send", label = "Invite" - ).compact.when(inviteState.get =!= CreateInviteProcess.Done) + ).compact.when(createInviteStatus.get =!= CreateInviteStatus.Done) ) ) ).addModifiers(Seq(ExploreStyles.CompactOverlayPanel, ExploreStyles.InviteUserPopup)) - .withRef(props.ref.ref) + .withRef(props.overlayRef.ref) diff --git a/explore/src/main/scala/explore/users/ProgramUserInvitations.scala b/explore/src/main/scala/explore/users/ProgramUserInvitations.scala index 22e18548a6..7cd67f108d 100644 --- a/explore/src/main/scala/explore/users/ProgramUserInvitations.scala +++ b/explore/src/main/scala/explore/users/ProgramUserInvitations.scala @@ -28,9 +28,14 @@ import lucuma.ui.primereact.* import lucuma.ui.syntax.all.given import lucuma.ui.table.* import queries.common.InvitationQueriesGQL.* +import cats.data.NonEmptySet +import lucuma.core.enums.ProgramUserRole -case class ProgramUserInvitations(invitations: View[List[UserInvitation]], readonly: Boolean) - extends ReactFnProps(ProgramUserInvitations.component) +case class ProgramUserInvitations( + invitations: View[List[UserInvitation]], + filterRoles: NonEmptySet[ProgramUserRole], + readonly: Boolean +) extends ReactFnProps(ProgramUserInvitations.component) object ProgramUserInvitations: private type Props = ProgramUserInvitations @@ -122,7 +127,8 @@ object ProgramUserInvitations: .useMemoBy((_, _, _) => ()): (_, ctx, _) => // cols _ => columns(ctx) .useMemoBy((props, _, _, _) => // rows - props.invitations.get.filter(_.status === InvitationStatus.Pending) + props.invitations.get.filter: i => + i.status === InvitationStatus.Pending && props.filterRoles.contains_(i.role) )((_, _, _, _) => identity) .useReactTableBy: (props, _, isActive, cols, rows) => TableOptions( @@ -136,11 +142,9 @@ object ProgramUserInvitations: ) ) .render: (props, _, _, _, _, table) => - React.Fragment( - PrimeTable( - table, - striped = true, - compact = Compact.Very, - tableMod = ExploreStyles.ExploreTable - ) + PrimeTable( + table, + striped = true, + compact = Compact.Very, + tableMod = ExploreStyles.ExploreTable ) diff --git a/explore/src/main/scala/explore/users/ProgramUsersTable.scala b/explore/src/main/scala/explore/users/ProgramUsersTable.scala index 9450cad89f..a666ee2ac9 100644 --- a/explore/src/main/scala/explore/users/ProgramUsersTable.scala +++ b/explore/src/main/scala/explore/users/ProgramUsersTable.scala @@ -44,10 +44,14 @@ import lucuma.ui.table.* import lucuma.ui.utils.* import monocle.function.Each.* import queries.common.ProposalQueriesGQL.UnlinkUser +import explore.model.UserInvitation +import lucuma.core.enums.InvitationStatus +import lucuma.ui.react.given case class ProgramUsersTable( programId: Program.Id, users: View[List[ProgramUserWithRole]], + invitations: View[List[UserInvitation]], filterRoles: NonEmptySet[ProgramUserRole], readonly: Boolean, hiddenColumns: Set[ProgramUsersTable.Column] = Set.empty @@ -284,9 +288,24 @@ object ProgramUsersTable: ) ) .render: (props, _, _, _, _, table) => - PrimeTable( - table, - striped = true, - compact = Compact.Very, - emptyMessage = "No users defined" + val arePendingInvitations: Boolean = props.invitations.get + .filter: i => + i.status === InvitationStatus.Pending && props.filterRoles.contains_(i.role) + .nonEmpty + + React.Fragment( + PrimeTable( + table, + striped = true, + compact = Compact.Very, + emptyMessage = "No users defined" + ), + Option + .when[VdomNode](arePendingInvitations)( + React.Fragment( + <.label("Pending invitations"), + ProgramUserInvitations(props.invitations, props.filterRoles, props.readonly) + ) + ) + .orEmpty ) From d0bcbe95bada8722dcfcfbb4a95d37550b46a0f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ra=C3=BAl=20Piaggio?= Date: Wed, 18 Sep 2024 22:44:31 -0300 Subject: [PATCH 09/11] all working, missing styles --- .../scala/explore/programs/SupportUsers.scala | 3 ++- .../explore/proposal/ProposalEditor.scala | 10 +++++----- .../scala/explore/users/InviteUserButton.scala | 18 +++++++++--------- .../scala/explore/users/InviteUserPopup.scala | 6 +++--- .../explore/users/ProgramUserInvitations.scala | 7 +++++-- .../explore/users/ProgramUsersTable.scala | 6 +++--- 6 files changed, 27 insertions(+), 23 deletions(-) diff --git a/explore/src/main/scala/explore/programs/SupportUsers.scala b/explore/src/main/scala/explore/programs/SupportUsers.scala index 98e8b0024b..4778dfa09b 100644 --- a/explore/src/main/scala/explore/programs/SupportUsers.scala +++ b/explore/src/main/scala/explore/programs/SupportUsers.scala @@ -7,13 +7,13 @@ import cats.data.NonEmptySet import crystal.react.View 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 -import explore.users.InviteUserButton case class SupportUsers( programId: Program.Id, @@ -36,6 +36,7 @@ object SupportUsers: <.label( props.title, InviteUserButton(props.programId, props.supportRole.role, props.invitations) + .unless(props.readonly) ), ProgramUsersTable( props.programId, diff --git a/explore/src/main/scala/explore/proposal/ProposalEditor.scala b/explore/src/main/scala/explore/proposal/ProposalEditor.scala index 4f052d8a64..97473afb78 100644 --- a/explore/src/main/scala/explore/proposal/ProposalEditor.scala +++ b/explore/src/main/scala/explore/proposal/ProposalEditor.scala @@ -3,8 +3,9 @@ package explore.proposal -import cats.syntax.option.* +import cats.data.NonEmptySet import cats.effect.IO +import cats.syntax.option.* import clue.* import clue.data.Input import clue.data.syntax.* @@ -27,6 +28,8 @@ import explore.model.UserInvitation import explore.model.enums.GridLayoutSection import explore.model.layout.LayoutsMap import explore.undo.* +import explore.users.InviteUserButton +import explore.users.ProgramUsersTable import japgolly.scalajs.react.* import japgolly.scalajs.react.vdom.html_<^.* import lucuma.core.enums.ProgramUserRole @@ -40,13 +43,10 @@ import lucuma.schemas.ObservationDB.Types.* import lucuma.ui.optics.* import lucuma.ui.primereact.* import lucuma.ui.primereact.given +import lucuma.ui.react.given import lucuma.ui.syntax.all.given import monocle.Iso import queries.common.ProposalQueriesGQL -import explore.users.InviteUserButton -import lucuma.ui.react.given -import explore.users.ProgramUsersTable -import cats.data.NonEmptySet case class ProposalEditor( programId: Program.Id, diff --git a/explore/src/main/scala/explore/users/InviteUserButton.scala b/explore/src/main/scala/explore/users/InviteUserButton.scala index a47e78bbab..2fc49363ee 100644 --- a/explore/src/main/scala/explore/users/InviteUserButton.scala +++ b/explore/src/main/scala/explore/users/InviteUserButton.scala @@ -4,20 +4,20 @@ package explore.users import crystal.react.View +import crystal.react.hooks.* +import explore.Icons +import explore.model.UserInvitation +import explore.model.display.given import japgolly.scalajs.react.* import japgolly.scalajs.react.vdom.html_<^.* +import lucuma.core.enums.ProgramUserRole +import lucuma.core.model.Program +import lucuma.core.syntax.display.* import lucuma.react.common.ReactFnProps import lucuma.react.primereact.Button +import lucuma.react.primereact.hooks.all.* import lucuma.ui.primereact.* import lucuma.ui.syntax.all.given -import explore.Icons -import lucuma.react.primereact.hooks.all.* -import lucuma.core.enums.ProgramUserRole -import explore.model.display.given -import lucuma.core.syntax.display.* -import lucuma.core.model.Program -import explore.model.UserInvitation -import crystal.react.hooks.* case class InviteUserButton( programId: Program.Id, @@ -37,7 +37,7 @@ object InviteUserButton: React.Fragment( InviteUserPopup( props.programId, - ProgramUserRole.Coi, + props.role, props.invitations, createInviteStatus, overlayRef diff --git a/explore/src/main/scala/explore/users/InviteUserPopup.scala b/explore/src/main/scala/explore/users/InviteUserPopup.scala index bf0364b579..67f21ff334 100644 --- a/explore/src/main/scala/explore/users/InviteUserPopup.scala +++ b/explore/src/main/scala/explore/users/InviteUserPopup.scala @@ -16,12 +16,14 @@ import explore.Icons import explore.components.ui.ExploreStyles import explore.model.AppContext import explore.model.UserInvitation +import explore.model.display.given import japgolly.scalajs.react.* import japgolly.scalajs.react.vdom.html_<^.* import lucuma.core.data.EmailAddress import lucuma.core.data.EmailPred import lucuma.core.enums.ProgramUserRole import lucuma.core.model.Program +import lucuma.core.syntax.display.* import lucuma.core.validation.InputValidSplitEpi import lucuma.react.common.ReactFnProps import lucuma.react.primereact.Button @@ -38,8 +40,6 @@ import lucuma.ui.syntax.all.given import org.typelevel.log4cats.Logger import queries.common.InvitationQueriesGQL.* import queries.common.InvitationQueriesGQL.CreateInviteMutation.Data -import lucuma.core.syntax.display.* -import explore.model.display.given case class InviteUserPopup( programId: Program.Id, @@ -92,7 +92,7 @@ object InviteUserPopup: onHide = key.set(None) >> emailView.set(None).runAsyncAndForget )( <.div(PrimeStyles.Dialog)( - <.div(PrimeStyles.DialogHeader, s"Create ${props.role.shortName} invitation"), + <.div(PrimeStyles.DialogHeader)(s"Create ${props.role.shortName} invitation"), <.div(PrimeStyles.DialogContent)( <.div(LucumaPrimeStyles.FormColumnCompact)( FormInputTextView( diff --git a/explore/src/main/scala/explore/users/ProgramUserInvitations.scala b/explore/src/main/scala/explore/users/ProgramUserInvitations.scala index 7cd67f108d..6fec8fd32b 100644 --- a/explore/src/main/scala/explore/users/ProgramUserInvitations.scala +++ b/explore/src/main/scala/explore/users/ProgramUserInvitations.scala @@ -3,6 +3,7 @@ package explore.users +import cats.data.NonEmptySet import cats.effect.IO import cats.syntax.all.* import crystal.* @@ -18,6 +19,7 @@ import explore.model.reusability.given import japgolly.scalajs.react.* import japgolly.scalajs.react.vdom.html_<^.* import lucuma.core.enums.InvitationStatus +import lucuma.core.enums.ProgramUserRole import lucuma.react.common.ReactFnProps import lucuma.react.floatingui.syntax.* import lucuma.react.primereact.* @@ -28,8 +30,6 @@ import lucuma.ui.primereact.* import lucuma.ui.syntax.all.given import lucuma.ui.table.* import queries.common.InvitationQueriesGQL.* -import cats.data.NonEmptySet -import lucuma.core.enums.ProgramUserRole case class ProgramUserInvitations( invitations: View[List[UserInvitation]], @@ -139,6 +139,9 @@ object ProgramUserInvitations: isActive = isActive, invitations = props.invitations, readOnly = props.readonly + ), + state = PartialTableState(columnVisibility = + ColumnVisibility(RevokeId -> Visibility.fromVisible(!props.readonly)) ) ) .render: (props, _, _, _, _, table) => diff --git a/explore/src/main/scala/explore/users/ProgramUsersTable.scala b/explore/src/main/scala/explore/users/ProgramUsersTable.scala index a666ee2ac9..c96c5eb32c 100644 --- a/explore/src/main/scala/explore/users/ProgramUsersTable.scala +++ b/explore/src/main/scala/explore/users/ProgramUsersTable.scala @@ -16,12 +16,14 @@ import explore.components.ui.PartnerFlags import explore.model.AppContext import explore.model.IsActive import explore.model.ProgramUserWithRole +import explore.model.UserInvitation import explore.model.display.given import explore.model.reusability.given import japgolly.scalajs.react.* import japgolly.scalajs.react.vdom.html_<^.* import lucuma.core.enums.EducationalStatus import lucuma.core.enums.Gender +import lucuma.core.enums.InvitationStatus import lucuma.core.enums.Partner import lucuma.core.enums.ProgramUserRole import lucuma.core.model.PartnerLink @@ -39,14 +41,12 @@ import lucuma.refined.* import lucuma.schemas.ObservationDB.Types.UnlinkUserInput import lucuma.ui.primereact.* import lucuma.ui.primereact.given +import lucuma.ui.react.given import lucuma.ui.syntax.all.given import lucuma.ui.table.* import lucuma.ui.utils.* import monocle.function.Each.* import queries.common.ProposalQueriesGQL.UnlinkUser -import explore.model.UserInvitation -import lucuma.core.enums.InvitationStatus -import lucuma.ui.react.given case class ProgramUsersTable( programId: Program.Id, From 9cff99d26625f742237b0cdc083e0551068fada0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ra=C3=BAl=20Piaggio?= Date: Wed, 18 Sep 2024 23:09:02 -0300 Subject: [PATCH 10/11] support users styling --- .../explore/components/ui/ExploreStyles.scala | 2 + common/src/main/webapp/sass/explore.scss | 21 ++++++++-- .../explore/programs/ProgramDetailsTile.scala | 2 +- .../scala/explore/programs/SupportUsers.scala | 3 +- .../explore/proposal/InvestigatorUsers.scala | 42 ------------------- .../users/ProgramUserInvitations.scala | 30 ++++++------- .../explore/users/ProgramUsersTable.scala | 2 +- 7 files changed, 36 insertions(+), 66 deletions(-) delete mode 100644 explore/src/main/scala/explore/proposal/InvestigatorUsers.scala diff --git a/common/src/main/scala/explore/components/ui/ExploreStyles.scala b/common/src/main/scala/explore/components/ui/ExploreStyles.scala index f11809ec85..6dcd3c7bbc 100644 --- a/common/src/main/scala/explore/components/ui/ExploreStyles.scala +++ b/common/src/main/scala/explore/components/ui/ExploreStyles.scala @@ -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") diff --git a/common/src/main/webapp/sass/explore.scss b/common/src/main/webapp/sass/explore.scss index b36a7a4528..d501bce2b0 100644 --- a/common/src/main/webapp/sass/explore.scss +++ b/common/src/main/webapp/sass/explore.scss @@ -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; + } + } } } diff --git a/explore/src/main/scala/explore/programs/ProgramDetailsTile.scala b/explore/src/main/scala/explore/programs/ProgramDetailsTile.scala index fe2428685c..1136cf5047 100644 --- a/explore/src/main/scala/explore/programs/ProgramDetailsTile.scala +++ b/explore/src/main/scala/explore/programs/ProgramDetailsTile.scala @@ -43,7 +43,7 @@ object ProgramDetailsTile: 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"), diff --git a/explore/src/main/scala/explore/programs/SupportUsers.scala b/explore/src/main/scala/explore/programs/SupportUsers.scala index 4778dfa09b..8557ca0e10 100644 --- a/explore/src/main/scala/explore/programs/SupportUsers.scala +++ b/explore/src/main/scala/explore/programs/SupportUsers.scala @@ -5,6 +5,7 @@ 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 @@ -32,7 +33,7 @@ object SupportUsers: case Secondary extends SupportRole(ProgramUserRole.SupportSecondary) private val component = ScalaFnComponent[Props]: props => - <.div( + <.div(ExploreStyles.ProgramDetailsUsers)( <.label( props.title, InviteUserButton(props.programId, props.supportRole.role, props.invitations) diff --git a/explore/src/main/scala/explore/proposal/InvestigatorUsers.scala b/explore/src/main/scala/explore/proposal/InvestigatorUsers.scala deleted file mode 100644 index 79a4080098..0000000000 --- a/explore/src/main/scala/explore/proposal/InvestigatorUsers.scala +++ /dev/null @@ -1,42 +0,0 @@ -// 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.data.NonEmptySet -import crystal.react.* -import explore.model.ProgramUserWithRole -import explore.model.UserInvitation -import explore.users.CreateInviteStatus -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.core.util.NewType -import lucuma.react.common.ReactFnProps -import lucuma.ui.syntax.all.given - -case class InvestigatorUsers( - pid: Program.Id, - readonly: Boolean, - users: View[List[ProgramUserWithRole]], - invitations: View[List[UserInvitation]], - private val state: View[InvestigatorUsers.State] -) extends ReactFnProps(InvestigatorUsers.component) - -object InvestigatorUsers: - private type Props = InvestigatorUsers - - object State extends NewType[CreateInviteStatus] - type State = State.Type - - private val component = - ScalaFnComponent[Props]: props => - ProgramUsersTable( - props.pid, - props.users, - props.invitations, - NonEmptySet.of(ProgramUserRole.Pi, ProgramUserRole.Coi, ProgramUserRole.CoiRO), - props.readonly - ) diff --git a/explore/src/main/scala/explore/users/ProgramUserInvitations.scala b/explore/src/main/scala/explore/users/ProgramUserInvitations.scala index 6fec8fd32b..7a8ad55bf7 100644 --- a/explore/src/main/scala/explore/users/ProgramUserInvitations.scala +++ b/explore/src/main/scala/explore/users/ProgramUserInvitations.scala @@ -48,23 +48,19 @@ object ProgramUserInvitations: private val ColDef = ColumnDef.WithTableMeta[UserInvitation, TableMeta] - private val KeyId: ColumnId = ColumnId("id") - private val EmailId: ColumnId = ColumnId("email") - private val EmailStatusId: ColumnId = ColumnId("emailStatus") - private val RevokeId: ColumnId = ColumnId("revoke") + private enum Column(val tag: String, val header: String): + val id: ColumnId = ColumnId(tag) - private val columnNames: Map[ColumnId, String] = Map( - KeyId -> "ID", - EmailId -> "email", - EmailStatusId -> "", - RevokeId -> "" - ) + case Key extends Column("id", "Id") + case Email extends Column("email", "Email") + case EmailStatus extends Column("emailStatus", "") + case Revoke extends Column("revoke", "") private def column[V]( - id: ColumnId, + column: Column, accessor: UserInvitation => V ): ColumnDef.Single.WithTableMeta[UserInvitation, V, TableMeta] = - ColDef(id, accessor, columnNames(id)) + ColDef(column.id, accessor, column.header) private def columns( ctx: AppContext[IO] @@ -72,10 +68,10 @@ object ProgramUserInvitations: import ctx.given List( - column(KeyId, _.id), - column(EmailId, _.email), + column(Column.Key, _.id), + column(Column.Email, _.email), ColDef( - EmailStatusId, + Column.EmailStatus.id, _.emailStatus, "Email Status", cell = _.value @@ -83,7 +79,7 @@ object ProgramUserInvitations: .getOrElse(<.span()) ), ColDef( - RevokeId, + Column.Revoke.id, identity, "Revoke", cell = cell => @@ -141,7 +137,7 @@ object ProgramUserInvitations: readOnly = props.readonly ), state = PartialTableState(columnVisibility = - ColumnVisibility(RevokeId -> Visibility.fromVisible(!props.readonly)) + ColumnVisibility(Column.Revoke.id -> Visibility.fromVisible(!props.readonly)) ) ) .render: (props, _, _, _, _, table) => diff --git a/explore/src/main/scala/explore/users/ProgramUsersTable.scala b/explore/src/main/scala/explore/users/ProgramUsersTable.scala index c96c5eb32c..c5bdce74fd 100644 --- a/explore/src/main/scala/explore/users/ProgramUsersTable.scala +++ b/explore/src/main/scala/explore/users/ProgramUsersTable.scala @@ -72,7 +72,7 @@ object ProgramUsersTable: enum Column( protected[ProgramUsersTable] val tag: String, protected[ProgramUsersTable] val header: String - ) derives Enumerated: + ): val id: ColumnId = ColumnId(tag) case Name extends Column("name", "Name") From 622bd92ed8fca0468dfae900c758d7cbb6d8853e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ra=C3=BAl=20Piaggio?= Date: Wed, 18 Sep 2024 23:22:06 -0300 Subject: [PATCH 11/11] prettier --- common/src/main/webapp/sass/explore.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/src/main/webapp/sass/explore.scss b/common/src/main/webapp/sass/explore.scss index d501bce2b0..7897b77537 100644 --- a/common/src/main/webapp/sass/explore.scss +++ b/common/src/main/webapp/sass/explore.scss @@ -3252,7 +3252,7 @@ a.explore-upgrade-link { display: grid; grid-template-columns: [label] auto [value] 1fr; gap: 0.25em 1em; - grid-auto-rows: max-content; + grid-auto-rows: max-content; } label {