From 0aba8912ca20fbded20bd0eb83c6be3658802417 Mon Sep 17 00:00:00 2001 From: Todd Burnside Date: Tue, 28 Jan 2025 12:22:23 -0800 Subject: [PATCH 1/2] Allow for changing between readonly and full access for CoIs. --- .../queries/common/ProgramQueriesGQL.scala | 12 +++++++ .../scala/explore/common/ProgramQueries.scala | 9 ++++++ .../explore/proposal/ProposalEditor.scala | 9 ++---- ...utton.scala => AddReadonlyCoiButton.scala} | 20 +++++------- .../explore/users/ProgramUsersTable.scala | 31 ++++++++++++++++--- .../scala/explore/model/ProgramUser.scala | 2 ++ 6 files changed, 60 insertions(+), 23 deletions(-) rename explore/src/main/scala/explore/users/{AddProgramUserButton.scala => AddReadonlyCoiButton.scala} (77%) diff --git a/explore/src/clue/scala/queries/common/ProgramQueriesGQL.scala b/explore/src/clue/scala/queries/common/ProgramQueriesGQL.scala index 0a942e53bf..63bce9a5ec 100644 --- a/explore/src/clue/scala/queries/common/ProgramQueriesGQL.scala +++ b/explore/src/clue/scala/queries/common/ProgramQueriesGQL.scala @@ -128,6 +128,18 @@ object ProgramQueriesGQL: } """ + @GraphQL + trait ChangeProgramUserRoleMutation extends GraphQLOperation[ObservationDB]: + val document = s""" + mutation($$input: ChangeProgramUserRoleInput!) { + changeProgramUserRole(input: $$input) { + programUser { + role + } + } + } + """ + @GraphQL trait UpdateConfigurationRequestsMutation extends GraphQLOperation[ObservationDB]: val document = s""" diff --git a/explore/src/main/scala/explore/common/ProgramQueries.scala b/explore/src/main/scala/explore/common/ProgramQueries.scala index b724ea3c2b..137127c8e1 100644 --- a/explore/src/main/scala/explore/common/ProgramQueries.scala +++ b/explore/src/main/scala/explore/common/ProgramQueries.scala @@ -150,6 +150,15 @@ object ProgramQueries: )(using FetchClient[F, ObservationDB]): F[Unit] = updateProgramUsers(puid, ProgramUserPropertiesInput(gender = g.orUnassign)) + def changeProgramUserRole[F[_]: Async](puid: ProgramUser.Id, role: ProgramUserRole)(using + FetchClient[F, ObservationDB] + ): F[Unit] = + ChangeProgramUserRoleMutation[F] + .execute: + ChangeProgramUserRoleInput(programUserId = puid, newRole = role) + .raiseGraphQLErrors + .void + // Note: If justification is none, it is ignored, not un-set. We // (currently, at least) do not allow unsetting justifications in explore. def updateConfigurationRequestStatus[F[_]: Async]( diff --git a/explore/src/main/scala/explore/proposal/ProposalEditor.scala b/explore/src/main/scala/explore/proposal/ProposalEditor.scala index 766741edee..51fc25161d 100644 --- a/explore/src/main/scala/explore/proposal/ProposalEditor.scala +++ b/explore/src/main/scala/explore/proposal/ProposalEditor.scala @@ -31,7 +31,7 @@ import explore.model.ProposalTabTileIds import explore.model.enums.GridLayoutSection import explore.model.layout.LayoutsMap import explore.undo.* -import explore.users.AddProgramUserButton +import explore.users.AddReadonlyCoiButton import explore.users.ProgramUsersTable import japgolly.scalajs.react.* import japgolly.scalajs.react.vdom.html_<^.* @@ -153,12 +153,7 @@ object ProposalEditor: .unless[VdomNode](props.proposalOrUserIsReadonly): <.div( ExploreStyles.AddProgramUserButton, - AddProgramUserButton(props.programId, - ProgramUserRole.CoiRO, - props.users, - icon = Icons.UserMagnifyingGlass - ), - AddProgramUserButton(props.programId, ProgramUserRole.Coi, props.users) + AddReadonlyCoiButton(props.programId, props.users) ) .orEmpty ) diff --git a/explore/src/main/scala/explore/users/AddProgramUserButton.scala b/explore/src/main/scala/explore/users/AddReadonlyCoiButton.scala similarity index 77% rename from explore/src/main/scala/explore/users/AddProgramUserButton.scala rename to explore/src/main/scala/explore/users/AddReadonlyCoiButton.scala index 288cbf7b37..c9ea79180e 100644 --- a/explore/src/main/scala/explore/users/AddProgramUserButton.scala +++ b/explore/src/main/scala/explore/users/AddReadonlyCoiButton.scala @@ -10,13 +10,11 @@ import explore.Icons import explore.model.AppContext import explore.model.IsActive import explore.model.ProgramUser -import explore.model.display.given import explore.syntax.ui.* 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.ReactFnComponent import lucuma.react.common.ReactFnProps import lucuma.react.fa.FontAwesomeIcon @@ -26,15 +24,13 @@ import lucuma.schemas.ObservationDB.Types.AddProgramUserInput import lucuma.ui.primereact.* import queries.common.ProposalQueriesGQL.* -case class AddProgramUserButton( +case class AddReadonlyCoiButton( programId: Program.Id, - role: ProgramUserRole, - users: View[List[ProgramUser]], - icon: FontAwesomeIcon = Icons.UserPlus -) extends ReactFnProps(AddProgramUserButton) + users: View[List[ProgramUser]] +) extends ReactFnProps(AddReadonlyCoiButton) -object AddProgramUserButton - extends ReactFnComponent[AddProgramUserButton](props => +object AddReadonlyCoiButton + extends ReactFnComponent[AddReadonlyCoiButton](props => for { ctx <- useContext(AppContext.ctx) isActive <- useStateView(IsActive(false)) @@ -42,7 +38,7 @@ object AddProgramUserButton import ctx.given def addProgramUser: IO[Unit] = - val input = AddProgramUserInput(programId = props.programId, role = props.role) + val input = AddProgramUserInput(programId = props.programId, role = ProgramUserRole.CoiRO) AddProgramUser[IO] .execute(input) .raiseGraphQLErrors @@ -53,8 +49,8 @@ object AddProgramUserButton Button( severity = Button.Severity.Secondary, loading = isActive.get.value, - icon = props.icon, - tooltip = s"Add ${props.role.longName}", + icon = Icons.UserPlus, + tooltip = "Add Co-Investigator", onClick = addProgramUser.runAsync ).tiny.compact ) diff --git a/explore/src/main/scala/explore/users/ProgramUsersTable.scala b/explore/src/main/scala/explore/users/ProgramUsersTable.scala index 725d026b0a..4fe02fcaef 100644 --- a/explore/src/main/scala/explore/users/ProgramUsersTable.scala +++ b/explore/src/main/scala/explore/users/ProgramUsersTable.scala @@ -244,6 +244,9 @@ object ProgramUsersTable: case InProgress(message) => <.span(Icons.Info).withTooltip(message) case Failed(message) => <.span(Icons.ErrorIcon).withTooltip(message) + private val CoIRoles: Set[ProgramUserRole] = Set(ProgramUserRole.Coi, ProgramUserRole.CoiRO) + private val NonCoIRoles: Set[ProgramUserRole] = Enumerated[ProgramUserRole].all.toSet -- CoIRoles + private def columns(using ctx: AppContext[IO] ): List[ColumnDef.WithTableMeta[View[ProgramUser], ?, TableMeta]] = @@ -338,7 +341,7 @@ object ProgramUsersTable: val canEdit = meta.currentUserCanEdit(cell.get) EnumDropdownOptionalView( - id = "es".refined, + id = NonEmptyString.unsafeFrom(s"$programUserId-es"), value = view, showClear = true, itemTemplate = _.value.shortName, @@ -361,7 +364,7 @@ object ProgramUsersTable: val canEdit = meta.currentUserCanEdit(cell.get) Checkbox( - id = "thesis", + id = s"$programUserId-thesis", checked = view.get.getOrElse(false), disabled = !canEdit || meta.isActive.get.value, onChange = r => view.set(r.some) @@ -382,7 +385,7 @@ object ProgramUsersTable: val canEdit = meta.currentUserCanEdit(cell.get) EnumOptionalDropdown[Gender]( - id = "gender".refined, + id = NonEmptyString.unsafeFrom(s"$programUserId-gender"), value = view.get, showClear = true, itemTemplate = _.value.shortName, @@ -395,7 +398,27 @@ object ProgramUsersTable: ).sortableBy(_.get.toString), column(Column.OrcidId, _.get.user.flatMap(_.orcidId).foldMap(_.value)).sortable, // TODO: Make editable between COI and Readonly COI, if user is not this one - column(Column.Role, _.get.role.shortName).sortable, + // column(Column.Role, _.get.role.shortName).sortable, + ColDef( + Column.Role.id, + _.zoom(ProgramUser.role), + Column.Role.header, + cell = c => + val currentRole = c.value.get + c.table.options.meta.map: meta => + if (meta.proposalOrUserIsReadonly || !CoIRoles.contains(currentRole)) + currentRole.shortName: VdomNode + else + val programUserId = c.row.original.get.id + val view = + c.value.withOnMod(role => changeProgramUserRole[IO](programUserId, role).runAsync) + EnumDropdownView( + id = NonEmptyString.unsafeFrom(s"$programUserId-role"), + value = view, + exclude = NonCoIRoles, + clazz = ExploreStyles.PartnerSelector + ): VdomNode + ).sortableBy(_.get.shortName), ColDef( Column.Status.id, _.get.status, diff --git a/model/shared/src/main/scala/explore/model/ProgramUser.scala b/model/shared/src/main/scala/explore/model/ProgramUser.scala index 695968e39d..264368affe 100644 --- a/model/shared/src/main/scala/explore/model/ProgramUser.scala +++ b/model/shared/src/main/scala/explore/model/ProgramUser.scala @@ -72,6 +72,8 @@ object ProgramUser: val partnerLink: Lens[ProgramUser, Option[PartnerLink]] = Focus[ProgramUser](_.partnerLink) + val role: Lens[ProgramUser, ProgramUserRole] = Focus[ProgramUser](_.role) + val educationalStatus: Lens[ProgramUser, Option[EducationalStatus]] = Focus[ProgramUser](_.educationalStatus) From b3387fb32e651780b04f26a65367f6adee304afc Mon Sep 17 00:00:00 2001 From: Todd Burnside Date: Tue, 28 Jan 2025 12:49:26 -0800 Subject: [PATCH 2/2] Remove commented line --- explore/src/main/scala/explore/users/ProgramUsersTable.scala | 1 - 1 file changed, 1 deletion(-) diff --git a/explore/src/main/scala/explore/users/ProgramUsersTable.scala b/explore/src/main/scala/explore/users/ProgramUsersTable.scala index 4fe02fcaef..c87c71e350 100644 --- a/explore/src/main/scala/explore/users/ProgramUsersTable.scala +++ b/explore/src/main/scala/explore/users/ProgramUsersTable.scala @@ -398,7 +398,6 @@ object ProgramUsersTable: ).sortableBy(_.get.toString), column(Column.OrcidId, _.get.user.flatMap(_.orcidId).foldMap(_.value)).sortable, // TODO: Make editable between COI and Readonly COI, if user is not this one - // column(Column.Role, _.get.role.shortName).sortable, ColDef( Column.Role.id, _.zoom(ProgramUser.role),