Skip to content

Commit

Permalink
Feat/current team project validation (#1456)
Browse files Browse the repository at this point in the history
* feat(currentTeamProjectValidation): Initial commit

* feat(currentTeamProjectValidation): Moved logic outside of separate component into TeamProjectHeader, should decrease total requests needed

* feat(currentTeamProjectValidation): refactored code so when in edit mode with an invalid teamProject application does not display the expired name

* feat(currentTeamProjectValidation): began refactoring failed test

* feat(currentTeamProjectValidation): Updated TeamProjectModel.test.jsx to pass all unit tests

* feat(dataDownloadListActions): Removed dead code: InvalidTeamProjectMessage

* feat(dataDownloadListActions): fixed lint issues

* feat(currentTeamProjectValidation): updated unit tests to pass

* feat(currentTeamProjectValidation): Updated story book stories

* feat(currentTeamProjectValidation): removed duplicate function call

* feat(currentTeamProjectValidation): Wrote unit test for IsCurrentTeamProjectValid

* feat(currentTeamProjectValidation): updated function name for clarity

* feat(currentTeamProjectValidation): removed console log and linted code

* feat(currentTeamProjectValidation): Formatted code for clarity

* feat(currentTeamProjectValidation): ran linter

* feat(currentTeamProjectValidation): refactored IsCurrentTeamProjectValid.test.js to use existing test data

* feat(currentTeamProjectValidation): removed unneeded localStorage set call

* feat(currentTeamProjectValidation): ran linter

* feat(currentTeamProjectValidation): cleaned up code for clarity
  • Loading branch information
jarvisraymond-uchicago committed Nov 27, 2023
1 parent 4d0c154 commit 56a8634
Show file tree
Hide file tree
Showing 10 changed files with 254 additions and 131 deletions.
7 changes: 4 additions & 3 deletions src/Analysis/AnalysisApp.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,9 @@ class AnalysisApp extends React.Component {
const TeamProject = localStorage.getItem('teamProject');
const regexp = /^\/\w[\w/]*$/gi;
const isValidTeamProject = new RegExp(regexp).test(TeamProject);
if (!isValidTeamProject) {
if (TeamProject && !isValidTeamProject) {
throw new Error(
`Found illegal "teamProject" parameter value ${TeamProject}`,
`Found illegal "teamProject" parameter value: ${TeamProject}`,
);
}
return `${this.state.app.applicationUrl}#/home?teamproject=${TeamProject}`;
Expand Down Expand Up @@ -161,7 +161,8 @@ class AnalysisApp extends React.Component {
title='Analysis App'
frameBorder='0'
src={
this.state.app.title === 'OHDSI Atlas' && this.state.app.needsTeamProject
this.state.app.title === 'OHDSI Atlas'
&& this.state.app.needsTeamProject
? this.getAtlasURLWithTeamProject()
: `${this.state.app.applicationUrl}`
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
const IsCurrentTeamProjectValid = (data) => {
if (!data.teams) {
return false;
}
let currentTeamProjectIsValid = false;
const currentTeamProject = localStorage.getItem('teamProject');
data.teams.forEach((team) => {
if (team.teamName === currentTeamProject) {
currentTeamProjectIsValid = true;
}
});
return currentTeamProjectIsValid;
};

export default IsCurrentTeamProjectValid;
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import IsCurrentTeamProjectValid from './IsCurrentTeamProjectValid';
import TeamProjectTestData from '../TestData/TeamProjectTestData';

const localStorageMock = {
getItem: jest.fn(),
setItem: jest.fn(),
};

beforeEach(() => {
jest.resetAllMocks();
Object.defineProperty(window, 'localStorage', {
value: localStorageMock,
writable: true,
});
});

describe('IsCurrentTeamProjectValid', () => {
it('should return false if data doesn\'t contain teams', () => {
const result = IsCurrentTeamProjectValid({ notTeams: [] });
expect(result).toBe(false);
});

it('should return false if current team project is not valid', () => {
localStorageMock.getItem.mockReturnValue('InvalidTeamName');
const result = IsCurrentTeamProjectValid(TeamProjectTestData.data);
expect(result).toBe(false);
expect(localStorageMock.getItem).toHaveBeenCalledWith('teamProject');
});

it('should return true if current team project is valid', () => {
localStorageMock.getItem.mockReturnValue(
TeamProjectTestData.data.teams[0].teamName,
);
const result = IsCurrentTeamProjectValid(TeamProjectTestData.data);
expect(result).toBe(true);
expect(localStorageMock.getItem).toHaveBeenCalledWith('teamProject');
});
});
Original file line number Diff line number Diff line change
@@ -1,30 +1,58 @@
import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import { useHistory } from 'react-router-dom';
import { useQuery } from 'react-query';
import EditIcon from './Icons/EditIcon';
import isEnterOrSpace from '../../IsEnterOrSpace';
import TeamProjectModal from '../TeamProjectModal/TeamProjectModal';
import queryConfig from '../../QueryConfig';
import fetchArboristTeamProjectRoles from '../Utils/teamProjectApi';
import IsCurrentTeamProjectValid from './IsCurrentTeamProjectValid';
import './TeamProjectHeader.css';

const TeamProjectHeader = ({ isEditable }) => {
const [isModalOpen, setIsModalOpen] = useState(false);
const [bannerText, setBannerText] = useState('- -');
const [selectedTeamProject, setSelectedTeamProject] = useState(
localStorage.getItem('teamProject'),
);
const showModal = () => {
setIsModalOpen(true);
};
const history = useHistory();

const rerouteToAppSelectionIfNeeded = () => {
if (!isEditable && !localStorage.getItem('teamProject')) {
// non-editable view should redirect to app selection if user doesn't have a storedTeamProject
history.push('/analysis');
}
};

const { data, status } = useQuery(
'teamprojects',
fetchArboristTeamProjectRoles,
queryConfig,
);

let currentTeamProjectIsValid = false;
if (data) {
currentTeamProjectIsValid = IsCurrentTeamProjectValid(data);
if (!currentTeamProjectIsValid) {
localStorage.removeItem('teamProject');
rerouteToAppSelectionIfNeeded();
}
}

useEffect(() => {
const storedTeamProject = localStorage.getItem('teamProject');
if (storedTeamProject) {
setBannerText(storedTeamProject);
} else if (isEditable) {
setSelectedTeamProject(null);
showModal();
} else if (!isEditable && !storedTeamProject) {
// non-editable view should redirect to app selection if user doesn't have a storedTeamProject
history.push('/analysis');
}
}, [history, isEditable]);
rerouteToAppSelectionIfNeeded();
}, [history, isEditable, currentTeamProjectIsValid, data]);

return (
<React.Fragment>
Expand Down Expand Up @@ -52,6 +80,10 @@ const TeamProjectHeader = ({ isEditable }) => {
isModalOpen={isModalOpen}
setIsModalOpen={setIsModalOpen}
setBannerText={setBannerText}
data={data}
status={status}
selectedTeamProject={selectedTeamProject}
setSelectedTeamProject={setSelectedTeamProject}
/>
)}
</React.Fragment>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React from 'react';
import TeamProjectHeader from './TeamProjectHeader';
import { QueryClient, QueryClientProvider } from 'react-query';
import { rest } from 'msw';

export default {
title: 'TESTS1/SharedUtils/TeamProjectHeader',
Expand All @@ -10,17 +11,66 @@ export default {
const Template = (args) => (
<div className='GWASApp'>
<QueryClientProvider client={new QueryClient()} contextSharing>
<TeamProjectHeader {...args}/>
</QueryClientProvider>,
<TeamProjectHeader {...args} />
</QueryClientProvider>
,
</div>
);

export const isEditable = Template.bind({});
isEditable.args = {
const successParameters = {
msw: {
handlers: [
rest.get('http://:arboristapi/authz/mapping', (req, res, ctx) => {
return res(
ctx.delay(3000),
ctx.json({
'/gwas_projects/project11': [
{
service: 'atlas-argo-wrapper-and-cohort-middleware',
method: 'access',
},
],
'/gwas_projects/project22': [
{
service: 'atlas-argo-wrapper-and-cohort-middleware',
method: 'access',
},
],
'/somethingelse': [
{
service: 'requestor',
method: 'create',
},
],
})
);
}),
],
},
};

export const MockedSuccessIsEditable = Template.bind({});
MockedSuccessIsEditable.args = {
isEditable: true,
};
MockedSuccessIsEditable.parameters = successParameters;

export const notEditable = Template.bind({});
notEditable.args = {
export const MockedSuccessIsNotEditable = Template.bind({});
MockedSuccessIsNotEditable.args = {
isEditable: false,
};
MockedSuccessIsNotEditable.parameters = successParameters;

export const MockedError403 = Template.bind({});
MockedError403.args = {
isEditable: true,
};
MockedError403.parameters = {
msw: {
handlers: [
rest.get('http://:arboristapi/authz/mapping', (req, res, ctx) =>
res(ctx.delay(800), ctx.status(403))
),
],
},
};
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@ import { Router } from 'react-router-dom';
import { QueryClient, QueryClientProvider } from 'react-query';
import '@testing-library/jest-dom';
import TeamProjectHeader from './TeamProjectHeader';
import TeamProjectTestData from '../TestData/TeamProjectTestData';

const localStorageMock = {
getItem: jest.fn(),
setItem: jest.fn(),
clear: jest.fn(),
};

beforeEach(() => {
Expand Down Expand Up @@ -57,20 +57,32 @@ test('renders TeamProjectHeader with edit button when isEditable is true and can
// Assert that the component renders with the edit button
expect(screen.queryByTestId('team-project-edit')).toBeInTheDocument();

// Simulate a click on the edit button and assert that the modal opens
// Simulate a click on the edit button and the modal with text "Team Projects" opens
fireEvent.click(screen.queryByTestId('team-project-edit'));
expect(screen.getByText('Team Projects')).toBeInTheDocument();
});

test('Renders project name based on local storage value', () => {
// Set up a mock value for localStorage.getItem('teamProject')
const teamProjectValue = 'Mock Team Project Name';
localStorageMock.getItem.mockReturnValueOnce(teamProjectValue);
test('renders TeamProjectHeader with team project name from localStorage', () => {
const testData = TeamProjectTestData;
// Mock the data response from react-query
jest.mock('react-query', () => ({
...jest.requireActual('react-query'),
useQuery: jest.fn(() => testData),
}));

Object.defineProperty(window, 'localStorage', {
value: localStorageMock,
});

const testName = TeamProjectTestData.data.teams[0].teamName;
// Set the localStorage variable for teamProject
localStorageMock.getItem.mockReturnValue(testName);

render(
<QueryClientProvider client={new QueryClient()} contextSharing>
<TeamProjectHeader />
<QueryClientProvider client={new QueryClient()}>
<TeamProjectHeader isEditable={false} />
</QueryClientProvider>,
);
// Assert that the component renders with the banner text from localStorage
expect(screen.getByText(`/ ${teamProjectValue}`)).toBeInTheDocument();

expect(screen.getByText(new RegExp(testName, 'i'))).toBeInTheDocument();
});
Original file line number Diff line number Diff line change
@@ -1,31 +1,26 @@
import React, { useState } from 'react';
import React from 'react';
import PropTypes from 'prop-types';
import {
Button, Modal, Spin, Select,
} from 'antd';
import { useQuery } from 'react-query';
import queryConfig from '../../QueryConfig';
import LoadingErrorMessage from '../../LoadingErrorMessage/LoadingErrorMessage';
import fetchArboristTeamProjectRoles from '../Utils/teamProjectApi';
import './TeamProjectModal.css';

const TeamProjectModal = ({ isModalOpen, setIsModalOpen, setBannerText }) => {
const [selectedTeamProject, setSelectedTeamProject] = useState(
localStorage.getItem('teamProject'),
);

const TeamProjectModal = ({
isModalOpen,
setIsModalOpen,
setBannerText,
data,
status,
selectedTeamProject,
setSelectedTeamProject,
}) => {
const closeAndUpdateTeamProject = () => {
setIsModalOpen(false);
setBannerText(selectedTeamProject);
localStorage.setItem('teamProject', selectedTeamProject);
};

const { data, status } = useQuery(
'teamprojects',
fetchArboristTeamProjectRoles,
queryConfig,
);

let modalContent = (
<Modal
open={isModalOpen}
Expand Down Expand Up @@ -98,13 +93,20 @@ const TeamProjectModal = ({ isModalOpen, setIsModalOpen, setBannerText }) => {
</Modal>
);
}
return <React.Fragment>{ modalContent }</React.Fragment>;
return <React.Fragment>{modalContent}</React.Fragment>;
};

TeamProjectModal.propTypes = {
isModalOpen: PropTypes.bool.isRequired,
setIsModalOpen: PropTypes.func.isRequired,
setBannerText: PropTypes.func.isRequired,
data: PropTypes.object,
status: PropTypes.string.isRequired,
selectedTeamProject: PropTypes.string,
setSelectedTeamProject: PropTypes.func.isRequired,
};
TeamProjectModal.defaultProps = {
data: PropTypes.null,
selectedTeamProject: PropTypes.null,
};

export default TeamProjectModal;
Loading

0 comments on commit 56a8634

Please sign in to comment.