Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Workflow to prevent deletion or removal of last PI #285

Merged
merged 3 commits into from
Dec 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading