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

CRDCDH-860 Optional Study Abbreviation #301

Merged
merged 8 commits into from
Mar 15, 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
2 changes: 1 addition & 1 deletion src/components/Contexts/FormContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ export const FormProvider: FC<ProviderProps> = ({ children, id } : ProviderProps
application: {
_id: newState?.data?.["_id"] === "new" ? undefined : newState?.data?.["_id"],
programName: data?.program?.name,
studyAbbreviation: data?.study?.abbreviation,
studyAbbreviation: data?.study?.abbreviation || data?.study?.name,
questionnaireData: JSON.stringify(data),
}
}
Expand Down
51 changes: 51 additions & 0 deletions src/components/Organizations/StudyTooltip.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import React, { ElementType, FC } from 'react';
import { Typography, styled } from '@mui/material';
import Tooltip from '../Tooltip';
import { formatFullStudyName } from '../../utils';

type Props = {
_id: Organization["_id"];
studies: Organization["studies"];
};

const StyledStudyCount = styled(Typography)<{ component: ElementType }>(({ theme }) => ({
textDecoration: "underline",
cursor: "pointer",
color: theme.palette.primary.main,
}));

const TooltipBody: FC<Props> = ({ _id, studies }) => (
<Typography variant="body1">
{studies?.map(({ studyName, studyAbbreviation }) => (
<React.Fragment key={`${_id}_study_${studyName}`}>
{formatFullStudyName(studyName, studyAbbreviation)}
<br />
</React.Fragment>
))}
</Typography>
);

/**
* Organization list view tooltip for studies
*
* @param Props
* @returns {React.FC}
*/
const StudyTooltip: FC<Props> = ({ _id, studies }) => (
<Tooltip
title={<TooltipBody _id={_id} studies={studies} />}
placement="top"
open={undefined}
onBlur={undefined}
disableHoverListener={false}
arrow
>
<StyledStudyCount variant="body2" component="span">
other
{" "}
{studies.length - 1}
</StyledStudyCount>
</Tooltip>
);

export default StudyTooltip;
41 changes: 3 additions & 38 deletions src/content/organizations/ListView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@ import {
import { Link, LinkProps, useLocation } from "react-router-dom";
import { Controller, useForm } from 'react-hook-form';
import PageBanner from "../../components/PageBanner";
import Tooltip from '../../components/Tooltip';
import { useOrganizationListContext, Status } from '../../components/Contexts/OrganizationListContext';
import SuspenseLoader from '../../components/SuspenseLoader';
import usePageTitle from '../../hooks/usePageTitle';
import StudyTooltip from '../../components/Organizations/StudyTooltip';

type T = Partial<Organization>;

Expand Down Expand Up @@ -162,12 +162,6 @@ const StyledTablePagination = styled(TablePagination)<{ component: React.Element
background: "#F5F7F8",
});

const StyledStudyCount = styled(Typography)<{ component: ElementType }>(({ theme }) => ({
textDecoration: "underline",
cursor: "pointer",
color: theme.palette.primary.main,
}));

const columns: Column[] = [
{
label: "Name",
Expand All @@ -188,24 +182,9 @@ const columns: Column[] = [

return (
<>
{studies[0].studyAbbreviation}
{studies[0].studyAbbreviation || studies[0].studyName}
{studies.length > 1 && " and "}
{studies.length > 1 && (
<Tooltip
title={<StudyContent _id={_id} studies={studies} />}
placement="top"
open={undefined}
onBlur={undefined}
disableHoverListener={false}
arrow
>
<StyledStudyCount variant="body2" component="span">
other
{" "}
{studies.length - 1}
</StyledStudyCount>
</Tooltip>
)}
{studies.length > 1 && (<StudyTooltip _id={_id} studies={studies} />)}
</>
);
},
Expand All @@ -227,20 +206,6 @@ const columns: Column[] = [
},
];

const StudyContent: FC<{ _id: Organization["_id"], studies: Organization["studies"] }> = ({ _id, studies }) => (
<Typography variant="body1">
{studies?.map(({ studyName, studyAbbreviation }) => (
<React.Fragment key={`${_id}_study_${studyName}`}>
{studyName}
{" ("}
{studyAbbreviation}
{") "}
<br />
</React.Fragment>
))}
</Typography>
);

/**
* View for List of Organizations
*
Expand Down
38 changes: 21 additions & 17 deletions src/content/organizations/OrganizationView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,16 +22,14 @@ import {
} from '../../graphql';
import ConfirmDialog from '../../components/Organizations/ConfirmDialog';
import usePageTitle from '../../hooks/usePageTitle';
import { formatFullStudyName, mapOrganizationStudyToId } from '../../utils';

type Props = {
_id: Organization["_id"] | "new";
};

type FormInput = Omit<EditOrganizationInput, "studies"> & {
/**
* Select boxes cannot contain objects, using `studyAbbreviation` instead
*/
studies: ApprovedStudy["studyAbbreviation"][];
studies: ApprovedStudy["_id"][];
};

const StyledContainer = styled(Container)({
Expand Down Expand Up @@ -172,6 +170,7 @@ const OrganizationView: FC<Props> = ({ _id }: Props) => {
const activeSubs = dataSubmissions?.filter((ds) => !inactiveSubmissionStatus.includes(ds?.status));

organization?.studies?.forEach((s) => {
// NOTE: The `Submission` type only has `studyAbbreviation`, we cannot compare IDs
if (activeSubs?.some((ds) => ds?.studyAbbreviation === s?.studyAbbreviation)) {
activeStudies[s?.studyAbbreviation] = true;
}
Expand All @@ -193,7 +192,7 @@ const OrganizationView: FC<Props> = ({ _id }: Props) => {
fetchPolicy: "cache-and-network",
});

const { data: approvedStudies } = useQuery<ListApprovedStudiesResp>(LIST_APPROVED_STUDIES, {
const { data: approvedStudies, refetch: refetchStudies } = useQuery<ListApprovedStudiesResp>(LIST_APPROVED_STUDIES, {
context: { clientName: 'backend' },
fetchPolicy: "cache-and-network",
});
Expand Down Expand Up @@ -239,14 +238,14 @@ const OrganizationView: FC<Props> = ({ _id }: Props) => {
const onSubmit = async (data: FormInput) => {
setSaving(true);

const studyAbbrToName: { [studyAbbreviation: string]: Pick<ApprovedStudy, "studyName" | "studyAbbreviation"> } = {};
approvedStudies?.listApprovedStudies?.forEach(({ studyName, studyAbbreviation }) => {
studyAbbrToName[studyAbbreviation] = { studyName, studyAbbreviation };
const studyMap: { [_id: string]: Pick<ApprovedStudy, "studyName" | "studyAbbreviation"> } = {};
approvedStudies?.listApprovedStudies?.forEach(({ _id, studyName, studyAbbreviation }) => {
studyMap[_id] = { studyName, studyAbbreviation };
});

const variables = {
...data,
studies: data.studies.map((abbr) => (studyAbbrToName[abbr])).filter((s) => !!s?.studyName && !!s?.studyAbbreviation),
studies: data.studies.map((_id) => (studyMap[_id]))?.filter((s) => !!s?.studyName) || [],
};

if (_id === "new" && !organization?._id) {
Expand Down Expand Up @@ -313,17 +312,25 @@ const OrganizationView: FC<Props> = ({ _id }: Props) => {

(async () => {
const { data, error } = await getOrganization({ variables: { orgID: _id, organization: _id } });

if (error || !data?.getOrganization) {
navigate("/organizations", { state: { error: "Unable to fetch organization" } });
return;
}

// No studies or original request did not complete. Refetch
let studyList: ApprovedStudy[] = approvedStudies?.listApprovedStudies;
if (!studyList?.length) {
const { data } = await refetchStudies();
studyList = data?.listApprovedStudies;
}

setOrganization(data?.getOrganization);
setDataSubmissions(data?.listSubmissions?.submissions);
setFormValues({
...data?.getOrganization,
studies: data?.getOrganization?.studies?.filter((s) => !!s?.studyName && !!s?.studyAbbreviation).map(({ studyAbbreviation }) => studyAbbreviation) || [],
studies: data?.getOrganization?.studies
?.map((s) => mapOrganizationStudyToId(s, studyList || []))
?.filter((_id) => !!_id) || [],
});
})();
}, [_id]);
Expand Down Expand Up @@ -413,12 +420,9 @@ const OrganizationView: FC<Props> = ({ _id }: Props) => {
inputProps={{ "aria-labelledby": "studiesLabel" }}
multiple
>
{approvedStudies?.listApprovedStudies?.map(({ studyName, studyAbbreviation }) => (
<MenuItem key={studyAbbreviation} value={studyAbbreviation}>
{studyName}
{" ("}
{studyAbbreviation}
{") "}
{approvedStudies?.listApprovedStudies?.map(({ _id, studyName, studyAbbreviation }) => (
<MenuItem key={_id} value={_id}>
{formatFullStudyName(studyName, studyAbbreviation)}
</MenuItem>
))}
</StyledSelect>
Expand Down
1 change: 0 additions & 1 deletion src/content/questionnaire/sections/B.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -379,7 +379,6 @@ const FormSectionB: FC<FormSectionProps> = ({ SectionOption, refs }: FormSection
readOnly={readOnlyInputs}
hideValidation={readOnlyInputs}
tooltipText="Provide a short abbreviation or acronym (e.g., NCI-MATCH) for the study."
required
/>
<TextInput
id="section-b-study-description"
Expand Down
1 change: 0 additions & 1 deletion src/types/ApprovedStudies.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ type ApprovedStudy = {
studyName: string;
/**
* Study Abbreviation
* This is a unique constraint across all studies
*
* @example GIS
*/
Expand Down
116 changes: 116 additions & 0 deletions src/utils/formUtils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -189,3 +189,119 @@ describe("programToSelectOption cases", () => {
expect(selectOption.value).toEqual("");
});
});

describe('formatFullStudyName cases', () => {
it('should return the study name with abbreviation if abbreviation is provided', () => {
const studyName = 'Study Name';
const studyAbbreviation = 'SN';
const result = utils.formatFullStudyName(studyName, studyAbbreviation);
expect(result).toBe('Study Name (SN)');
});

it('should return the study name without abbreviation if abbreviation is not provided', () => {
const studyName = 'Study Name';
const result = utils.formatFullStudyName(studyName, '');
expect(result).toBe('Study Name');
});

it('should return the study name without abbreviation if abbreviation is undefined', () => {
const studyName = 'Study Name';
const result = utils.formatFullStudyName(studyName, undefined);
expect(result).toBe('Study Name');
});

it('should remove extra spaces from the study name', () => {
const studyName = ' Study Name ';
const result = utils.formatFullStudyName(studyName, '');
expect(result).toBe('Study Name');
});

it('should remove extra spaces from the study abbreviation', () => {
const studyName = 'Study Name';
const studyAbbreviation = ' SN ';
const result = utils.formatFullStudyName(studyName, studyAbbreviation);
expect(result).toBe('Study Name (SN)');
});

it("should ignore the abbreviation if its equal to the study name", () => {
const studyName = 'Study Name';
const studyAbbreviation = 'Study Name';
const result = utils.formatFullStudyName(studyName, studyAbbreviation);
expect(result).toBe('Study Name');
});
});

describe('mapOrganizationStudyToId cases', () => {
it('should return the id of the matching study', () => {
const studies = [
{ _id: '1', studyName: 'Study 1', studyAbbreviation: 'S1' },
{ _id: '2', studyName: 'Study 2', studyAbbreviation: 'S2' },
] as ApprovedStudy[];

const study = { studyName: 'Study 1', studyAbbreviation: 'S1' };
const result = utils.mapOrganizationStudyToId(study, studies);

expect(result).toBe('1');
});

it("should return the first matching study's id", () => {
const studies = [
{ _id: '1', studyName: 'MATCH', studyAbbreviation: 'MA' },
{ _id: '2', studyName: 'Study 2', studyAbbreviation: 'S2' },
{ _id: '3', studyName: 'MATCH', studyAbbreviation: 'MA' },
] as ApprovedStudy[];

const study = { studyName: 'MATCH', studyAbbreviation: 'MA' };
const result = utils.mapOrganizationStudyToId(study, studies);

expect(result).toBe('1');
});

it('should return an empty string if no matching study is found', () => {
const studies = [
{ _id: '1', studyName: 'Study 1', studyAbbreviation: 'S1' },
{ _id: '2', studyName: 'Study 2', studyAbbreviation: 'S2' },
] as ApprovedStudy[];

const study = { studyName: 'Study 3', studyAbbreviation: 'S3' };
const result = utils.mapOrganizationStudyToId(study, studies);

expect(result).toBe('');
});

it("should not throw an exception if the study is undefined", () => {
const studies = [
{ _id: '1', studyName: 'Study 1', studyAbbreviation: 'S1' },
{ _id: '2', studyName: 'Study 2', studyAbbreviation: 'S2' },
] as ApprovedStudy[];

expect(() => utils.mapOrganizationStudyToId(undefined, studies)).not.toThrow();
});

it("should not throw an exception if the study is null", () => {
const studies = [
{ _id: '1', studyName: 'Study 1', studyAbbreviation: 'S1' },
{ _id: '2', studyName: 'Study 2', studyAbbreviation: 'S2' },
] as ApprovedStudy[];

expect(() => utils.mapOrganizationStudyToId(null, studies)).not.toThrow();
});

it("should not throw an exception if the approved studies are corrupt", () => {
const studies = [
null,
{ invalidObject: "true" },
{ AAAA: undefined },
] as unknown as ApprovedStudy[];

const study = { studyName: 'Study 1', studyAbbreviation: 'S1' };

expect(() => utils.mapOrganizationStudyToId(study, studies)).not.toThrow();
});

it("should not throw an exception if the approved studies are undefined", () => {
const study = { studyName: 'Study 1', studyAbbreviation: 'S1' };

expect(() => utils.mapOrganizationStudyToId(study, undefined)).not.toThrow();
});
});
Loading
Loading