Skip to content

Commit

Permalink
Merge pull request #285 from orppst/264-InvestigatorManagement
Browse files Browse the repository at this point in the history
Workflow to prevent deletion or removal of last PI
  • Loading branch information
Republicof1 authored Dec 9, 2024
2 parents ec6b288 + de95f0b commit 74488a0
Show file tree
Hide file tree
Showing 5 changed files with 262 additions and 10 deletions.
4 changes: 2 additions & 2 deletions src/main/webui/src/App2.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ import {
ScrollArea,
Group,
ActionIcon,
Tooltip, useMantineTheme, useMantineColorScheme, FileButton, Container
Tooltip, useMantineTheme, useMantineColorScheme, FileButton, Container,
} from '@mantine/core';
import {ColourSchemeToggle} from "./ColourSchemeToggle";
import {
Expand Down Expand Up @@ -457,7 +457,7 @@ function App2(): ReactElement {
}

function ProposalListWrapper(props:{proposalTitle: string, investigatorName:string, auth:boolean}) : ReactElement {

//console.log(props);
if (props.auth) {
return <ProposalList proposalTitle={props.proposalTitle} investigatorName={props.investigatorName} />
}
Expand Down
211 changes: 204 additions & 7 deletions src/main/webui/src/ProposalEditorView/investigators/List.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,27 @@
import { ReactElement, useState } from 'react';
import { ReactElement, useContext, useState } from 'react';
import {useNavigate, useParams} from "react-router-dom";
import {
fetchInvestigatorResourceRemoveInvestigator,
fetchInvestigatorResourceChangeInvestigatorKind,
fetchInvestigatorResourceGetInvestigators,
useInvestigatorResourceGetInvestigator,
useInvestigatorResourceGetInvestigators,
} from "src/generated/proposalToolComponents.ts";
import {useQueryClient} from "@tanstack/react-query";
import {Box, Grid, Stack, Table, Text} from "@mantine/core";
import {modals} from "@mantine/modals";

import {randomId} from "@mantine/hooks";
import DeleteButton from "src/commonButtons/delete";
import SwapRoleButton from 'src/commonButtons/swapRole';
import AddButton from "src/commonButtons/add";
import { JSON_SPACES } from 'src/constants.tsx';
import {EditorPanelHeader, PanelFrame} from "../../commonPanel/appearance.tsx";
import {notifyError} from "../../commonPanel/notifications.tsx";
import {ContextualHelpButton} from "../../commonButtons/contextualHelp.tsx"
import { InvestigatorKind, Person } from 'src/generated/proposalToolSchemas.ts';
import { ProposalContext } from 'src/App2.tsx';
import { useModals } from "@mantine/modals";


/**
* the data associated with a given person.
Expand All @@ -23,6 +30,18 @@ import {ContextualHelpButton} from "../../commonButtons/contextualHelp.tsx"
*/
type PersonProps = {
dbid: number
email?: string
}

type InvestigatorProps = {
dbid?: number
name?: string
}

type TypedInvestigator = {
person: Person
type: InvestigatorKind
_id: number
}

/**
Expand All @@ -38,7 +57,8 @@ function InvestigatorsPanel(): ReactElement {
{pathParams: { proposalCode: Number(selectedProposalCode)},},
{enabled: true});
const navigate = useNavigate();

const { user } = useContext(ProposalContext);


if (error) {
return (
Expand Down Expand Up @@ -69,6 +89,7 @@ function InvestigatorsPanel(): ReactElement {
{data?.map((item) => {
if(item.dbid !== undefined) {
return (<InvestigatorsRow dbid={item.dbid}
email={user.eMail}
key={item.dbid}/>)
} else {
return (
Expand Down Expand Up @@ -100,7 +121,6 @@ function InvestigatorsPanel(): ReactElement {
* header.
* @constructor
*/

function InvestigatorsHeader(): ReactElement {
return (
<>
Expand All @@ -112,8 +132,8 @@ function InvestigatorsHeader(): ReactElement {
<Table.Th>Name</Table.Th>
<Table.Th>eMail</Table.Th>
<Table.Th>Institute</Table.Th>
<Table.Th>Actions</Table.Th>
<Table.Th></Table.Th>

</Table.Tr>
</Table.Thead>

Expand All @@ -139,6 +159,7 @@ function InvestigatorsRow(props: PersonProps): ReactElement {
},
});
const queryClient = useQueryClient();


//Errors come in as name: "unknown", message: "Network Error" with an object
// called "stack" that contains the exception and message set in the API
Expand All @@ -149,6 +170,76 @@ function InvestigatorsRow(props: PersonProps): ReactElement {
setSubmitting(false);
}

/**
* count PIs
* @return number
*
*/
function CheckPiCount(delegateFucntion: Function) {

let PiProfile = 0;
let investigatorIDs = Array<number>();
setSubmitting(true);
fetchInvestigatorResourceGetInvestigators({
pathParams: {
proposalCode: Number(selectedProposalCode),
}
})
.then(()=>setSubmitting(false))
.then(()=>queryClient.invalidateQueries({
predicate: (query) => {

if(query.queryKey.length === 5)
{
const investigatorList = (query.state.data as Array<InvestigatorProps>);
if(typeof(investigatorList) == "object")
{
if(investigatorIDs.length < investigatorList.length){
investigatorList.forEach(inv => { investigatorIDs.push(inv.dbid as number) });
}
}
}

if(query.queryKey.length === 6)
{
//find the id of this object -
//see if its in our list
//if it is then we read the type
//if the type is PI we add it to the picount
//then we remove the index from investigatorID so we don't do more than once per item
const investigator = (query.state.data as TypedInvestigator)
const target = investigatorIDs.indexOf(investigator._id);
if(target >= 0)
{
console.log(investigator.type)
if(investigator.type == "PI")
{
PiProfile += 1;

}
console.log(PiProfile);
investigatorIDs[target] = -1;
}
}
return true;
}
}))
.finally(() => {
//if there are too few PI's prevent the action
if(PiProfile < 2)
{
lastPiContext();
}
//otherwise go for it
else{
delegateFucntion();
}


})
.catch(handleError);
}

/**
* handles the removal of an investigator.
*/
Expand All @@ -171,10 +262,74 @@ function InvestigatorsRow(props: PersonProps): ReactElement {
.catch(handleError);
}


/**
* handles the exchange of an investigator from PI to COI.
*/
function SwitchInvestigatorKind() {
var investigatorTypeSetting:InvestigatorKind = "COI";
if(data?.type == 'COI')
{
investigatorTypeSetting = "PI";

}
setSubmitting(true);
console.log(investigatorTypeSetting);
fetchInvestigatorResourceChangeInvestigatorKind({
pathParams: {
investigatorId: props.dbid,
proposalCode: Number(selectedProposalCode),
},
body: investigatorTypeSetting
})
.then(()=>setSubmitting(false))
.then(()=>queryClient.invalidateQueries({
predicate: (query) => {
// using 'length === 6' to ensure we get the set of investigators
return query.queryKey.length === 6 &&
query.queryKey[4] === 'investigators';
}
}))
.catch(handleError);
}

/**
* gives the user an option to verify if they wish to remove an
* investigator.
*/

function HandleSwap()
{
if(data?.type == "COI")
{
//if the target is a coi, allow swap to PI
return SwitchInvestigatorKind();
}
else{
//if the user is a PI, ensure there is another PI on the proposal
//if there is no other PI, prevent this
return CheckPiCount(SwitchInvestigatorKind);
}
}

function HandleDelete()
{
//COI or PI
if(data?.type == "COI")
{
//warn if the user is trying to remove themselves
if(data?.person?.eMail == props.email){
return openRemoveSelfModal();
}
//if the target is a coi, allow removal
return openRemoveModal();
}
//if the target is a PI, dont delete, but offer to swap to a COI
else {
return openSwitchModal();
}
}

const openRemoveModal = () =>
modals.openConfirmModal({
title: "Remove investigator",
Expand All @@ -189,6 +344,42 @@ function InvestigatorsRow(props: PersonProps): ReactElement {
onConfirm: () => handleRemove(),
});

const openRemoveSelfModal = () =>
modals.openConfirmModal({
title: "Warning, this user is you!",
centered: true,
children: (
<Text size="sm">
Removing yourself from the proposal will prevent you from accessing it in the future.<br/><br/>Be sure this is your inteded action before proceeding.
</Text>
),
labels: { confirm: "Remove myself from proposal", cancel: "Cancel" },
confirmProps: { color: "red" },
onConfirm: () => handleRemove(),
});

const openSwitchModal = () =>
modals.openConfirmModal({
title: "Confirm this action",
centered: true,
children: (
<Text size="sm">
You can't remove a PI from a proposal.<br/>Would you like to change {data?.person?.fullName} to a COI instead?
</Text>
),
labels: { confirm: "OK", cancel: "Cancel" },
confirmProps: { color: "green" },
onConfirm: () => CheckPiCount(SwitchInvestigatorKind),
});

const modals = useModals();
const lastPiContext = () =>
modals.openContextModal("investigator_modal", {
title: "Alert",
centered: true,
innerProps: "Proposals MUST have at least one PI. Another PI must be added before the action is allowed.",
});

// track error states
if (isLoading) {
return (
Expand All @@ -211,16 +402,22 @@ function InvestigatorsRow(props: PersonProps): ReactElement {

// return the full row.
return (
<>
<Table.Tr>
<Table.Td>{data?.type}</Table.Td>
<Table.Td>{data?.person?.fullName}</Table.Td>
<Table.Td>{data?.person?.eMail}</Table.Td>
<Table.Td>{data?.person?.homeInstitute?.name}</Table.Td>
<Table.Td><SwapRoleButton toolTipLabel={"swap role"}
onClick={HandleSwap}
/>
</Table.Td>
<Table.Td><DeleteButton toolTipLabel={"delete"}
onClick={openRemoveModal} />
onClick={HandleDelete} />
</Table.Td>
</Table.Tr>

</>
)
}

export default InvestigatorsPanel
38 changes: 38 additions & 0 deletions src/main/webui/src/commonButtons/swapRole.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { Button, Tooltip } from '@mantine/core';
import { IconUserPentagon } from '@tabler/icons-react';
import {
ClickButtonInterfaceProps
} from './buttonInterfaceProps.tsx';
import { ReactElement } from 'react';
import { CLOSE_DELAY, ICON_SIZE, OPEN_DELAY } from '../constants.tsx';


/**
* creates a swap role button.
*
* @param {ClickButtonInterfaceProps} props the button inputs.
* @return {ReactElement} the dynamic html for the delete button
* @constructor
*/
export default
function SwapRoleButton(props: ClickButtonInterfaceProps): ReactElement {
return (
<Tooltip
position={props.toolTipLabelPosition}
openDelay={OPEN_DELAY}
closeDelay={CLOSE_DELAY}
label={props.toolTipLabel}
>
<Button
rightSection={<IconUserPentagon size={ICON_SIZE}/>}
color={"green.5"}
variant={props.variant ?? "subtle"}
onClick={props.onClick ?? props.onClickEvent}
disabled={props.disabled}
//type="submit"
>
{props.label ?? 'Swap Role'}
</Button>
</Tooltip>
)
}
3 changes: 2 additions & 1 deletion src/main/webui/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {Notifications} from "@mantine/notifications";
import '@mantine/core/styles.css';
import '@mantine/notifications/styles.css';
import {QueryClient, QueryClientProvider} from "@tanstack/react-query";
import CustomModal from './util/Modal.tsx';

//if we want to override any parts of theme we can do it here
// this 'theme' object is merged with the 'theme' property of MantineProvider
Expand All @@ -27,7 +28,7 @@ function App() {

return (
<MantineProvider theme={theme}>
<ModalsProvider>
<ModalsProvider modals={{ investigator_modal: CustomModal }}>
<Notifications />
<QueryClientProvider client={queryClient}>
<App2/>
Expand Down
Loading

0 comments on commit 74488a0

Please sign in to comment.