Merge pull request #5786 from marshmalien/4951-org-team-links

Fix organization team links

Reviewed-by: https://github.com/apps/softwarefactory-project-zuul
This commit is contained in:
softwarefactory-project-zuul[bot]
2020-01-28 20:26:04 +00:00
committed by GitHub
6 changed files with 195 additions and 219 deletions

View File

@@ -30,7 +30,9 @@ function OrganizationTeams({ id, i18n }) {
data: { count = 0, results = [] }, data: { count = 0, results = [] },
} = await OrganizationsAPI.readTeams(id, params); } = await OrganizationsAPI.readTeams(id, params);
setItemCount(count); setItemCount(count);
setTeams(results); setTeams(
results.map(team => ({ ...team, url: `/teams/${team.id}/details` }))
);
} catch (error) { } catch (error) {
setContentError(error); setContentError(error);
} finally { } finally {

View File

@@ -2,7 +2,7 @@ import React from 'react';
import { act } from 'react-dom/test-utils'; import { act } from 'react-dom/test-utils';
import { OrganizationsAPI } from '@api'; import { OrganizationsAPI } from '@api';
import { mountWithContexts } from '@testUtils/enzymeHelpers'; import { mountWithContexts, waitForElement } from '@testUtils/enzymeHelpers';
import { sleep } from '@testUtils/testUtils'; import { sleep } from '@testUtils/testUtils';
import OrganizationTeams from './OrganizationTeams'; import OrganizationTeams from './OrganizationTeams';
@@ -65,7 +65,9 @@ describe('<OrganizationTeams />', () => {
wrapper.update(); wrapper.update();
const list = wrapper.find('PaginatedDataList'); const list = wrapper.find('PaginatedDataList');
expect(list.prop('items')).toEqual(listData.data.results); list.find('DataListCell').forEach((el, index) => {
expect(el.text()).toBe(listData.data.results[index].name);
});
expect(list.prop('itemCount')).toEqual(listData.data.count); expect(list.prop('itemCount')).toEqual(listData.data.count);
expect(list.prop('qsConfig')).toEqual({ expect(list.prop('qsConfig')).toEqual({
namespace: 'team', namespace: 'team',
@@ -78,4 +80,15 @@ describe('<OrganizationTeams />', () => {
integerFields: ['page', 'page_size'], integerFields: ['page', 'page_size'],
}); });
}); });
test('should show content error for failed instance group fetch', async () => {
OrganizationsAPI.readTeams.mockImplementationOnce(() =>
Promise.reject(new Error())
);
let wrapper;
await act(async () => {
wrapper = mountWithContexts(<OrganizationTeams id={1} />);
});
await waitForElement(wrapper, 'ContentError', el => el.length === 1);
});
}); });

View File

@@ -1,7 +1,14 @@
import React, { Component } from 'react'; import React, { useState, useEffect } from 'react';
import { withI18n } from '@lingui/react'; import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import { Switch, Route, withRouter, Redirect, Link } from 'react-router-dom'; import {
Link,
Redirect,
Route,
Switch,
useLocation,
useParams,
} from 'react-router-dom';
import { Card, PageSection } from '@patternfly/react-core'; import { Card, PageSection } from '@patternfly/react-core';
import CardCloseButton from '@components/CardCloseButton'; import CardCloseButton from '@components/CardCloseButton';
import { TabbedCardHeader } from '@components/Card'; import { TabbedCardHeader } from '@components/Card';
@@ -11,62 +18,31 @@ import TeamDetail from './TeamDetail';
import TeamEdit from './TeamEdit'; import TeamEdit from './TeamEdit';
import { TeamsAPI } from '@api'; import { TeamsAPI } from '@api';
class Team extends Component { function Team({ i18n, setBreadcrumb }) {
constructor(props) { const [team, setTeam] = useState(null);
super(props); const [contentError, setContentError] = useState(null);
const [hasContentLoading, setHasContentLoading] = useState(true);
const location = useLocation();
const { id } = useParams();
this.state = { useEffect(() => {
team: null, (async () => {
hasContentLoading: true,
contentError: null,
isInitialized: false,
};
this.loadTeam = this.loadTeam.bind(this);
}
async componentDidMount() {
await this.loadTeam();
this.setState({ isInitialized: true });
}
async componentDidUpdate(prevProps) {
const { location, match } = this.props;
const url = `/teams/${match.params.id}/`;
if (
prevProps.location.pathname.startsWith(url) &&
prevProps.location !== location &&
location.pathname === `${url}details`
) {
await this.loadTeam();
}
}
async loadTeam() {
const { match, setBreadcrumb } = this.props;
const id = parseInt(match.params.id, 10);
this.setState({ contentError: null, hasContentLoading: true });
try { try {
const { data } = await TeamsAPI.readDetail(id); const { data } = await TeamsAPI.readDetail(id);
setBreadcrumb(data); setBreadcrumb(data);
this.setState({ team: data }); setTeam(data);
} catch (err) { } catch (error) {
this.setState({ contentError: err }); setContentError(error);
} finally { } finally {
this.setState({ hasContentLoading: false }); setHasContentLoading(false);
} }
} })();
}, [id, setBreadcrumb]);
render() {
const { location, match, i18n } = this.props;
const { team, contentError, hasContentLoading, isInitialized } = this.state;
const tabsArray = [ const tabsArray = [
{ name: i18n._(t`Details`), link: `${match.url}/details`, id: 0 }, { name: i18n._(t`Details`), link: `/teams/${id}/details`, id: 0 },
{ name: i18n._(t`Users`), link: `${match.url}/users`, id: 1 }, { name: i18n._(t`Users`), link: `/teams/${id}/users`, id: 1 },
{ name: i18n._(t`Access`), link: `${match.url}/access`, id: 2 }, { name: i18n._(t`Access`), link: `/teams/${id}/access`, id: 2 },
]; ];
let cardHeader = ( let cardHeader = (
@@ -76,10 +52,6 @@ class Team extends Component {
</TabbedCardHeader> </TabbedCardHeader>
); );
if (!isInitialized) {
cardHeader = null;
}
if (location.pathname.endsWith('edit')) { if (location.pathname.endsWith('edit')) {
cardHeader = null; cardHeader = null;
} }
@@ -108,15 +80,14 @@ class Team extends Component {
<Switch> <Switch>
<Redirect from="/teams/:id" to="/teams/:id/details" exact /> <Redirect from="/teams/:id" to="/teams/:id/details" exact />
{team && ( {team && (
<Route <Route path="/teams/:id/details">
path="/teams/:id/edit" <TeamDetail team={team} />
render={() => <TeamEdit team={team} />} </Route>
/>
)} )}
{team && ( {team && (
<Route <Route
path="/teams/:id/details" path="/teams/:id/edit"
render={() => <TeamDetail team={team} />} render={() => <TeamEdit team={team} />}
/> />
)} )}
{team && ( {team && (
@@ -137,8 +108,8 @@ class Team extends Component {
render={() => render={() =>
!hasContentLoading && ( !hasContentLoading && (
<ContentError isNotFound> <ContentError isNotFound>
{match.params.id && ( {id && (
<Link to={`/teams/${match.params.id}/details`}> <Link to={`/teams/${id}/details`}>
{i18n._(`View Team Details`)} {i18n._(`View Team Details`)}
</Link> </Link>
)} )}
@@ -146,13 +117,11 @@ class Team extends Component {
) )
} }
/> />
,
</Switch> </Switch>
</Card> </Card>
</PageSection> </PageSection>
); );
} }
}
export default withI18n()(withRouter(Team)); export default withI18n()(Team);
export { Team as _Team }; export { Team as _Team };

View File

@@ -1,4 +1,5 @@
import React from 'react'; import React from 'react';
import { act } from 'react-dom/test-utils';
import { createMemoryHistory } from 'history'; import { createMemoryHistory } from 'history';
import { TeamsAPI } from '@api'; import { TeamsAPI } from '@api';
import { mountWithContexts, waitForElement } from '@testUtils/enzymeHelpers'; import { mountWithContexts, waitForElement } from '@testUtils/enzymeHelpers';
@@ -34,17 +35,28 @@ async function getTeams() {
} }
describe('<Team />', () => { describe('<Team />', () => {
test('initially renders succesfully', () => { let wrapper;
beforeEach(() => {
TeamsAPI.readDetail.mockResolvedValue({ data: mockTeam }); TeamsAPI.readDetail.mockResolvedValue({ data: mockTeam });
TeamsAPI.read.mockImplementation(getTeams); TeamsAPI.read.mockImplementation(getTeams);
mountWithContexts(<Team setBreadcrumb={() => {}} me={mockMe} />); });
test('initially renders succesfully', async () => {
await act(async () => {
wrapper = mountWithContexts(
<Team setBreadcrumb={() => {}} me={mockMe} />
);
});
expect(wrapper.find('Team').length).toBe(1);
}); });
test('should show content error when user attempts to navigate to erroneous route', async () => { test('should show content error when user attempts to navigate to erroneous route', async () => {
const history = createMemoryHistory({ const history = createMemoryHistory({
initialEntries: ['/teams/1/foobar'], initialEntries: ['/teams/1/foobar'],
}); });
const wrapper = mountWithContexts( await act(async () => {
wrapper = mountWithContexts(
<Team setBreadcrumb={() => {}} me={mockMe} />, <Team setBreadcrumb={() => {}} me={mockMe} />,
{ {
context: { context: {
@@ -62,6 +74,7 @@ describe('<Team />', () => {
}, },
} }
); );
});
await waitForElement(wrapper, 'ContentError', el => el.length === 1); await waitForElement(wrapper, 'ContentError', el => el.length === 1);
}); });
}); });

View File

@@ -1,2 +1,2 @@
export { default as TeamList } from './TeamList'; export { default } from './TeamList';
export { default as TeamListItem } from './TeamListItem'; export { default as TeamListItem } from './TeamListItem';

View File

@@ -1,37 +1,27 @@
import React, { Component, Fragment } from 'react'; import React, { useState, useCallback } from 'react';
import { Route, withRouter, Switch } from 'react-router-dom'; import { Route, Switch } from 'react-router-dom';
import { withI18n } from '@lingui/react'; import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import { Config } from '@contexts/Config'; import { Config } from '@contexts/Config';
import Breadcrumbs from '@components/Breadcrumbs/Breadcrumbs'; import Breadcrumbs from '@components/Breadcrumbs';
import TeamList from './TeamList';
import TeamsList from './TeamList/TeamList'; import TeamAdd from './TeamAdd';
import TeamAdd from './TeamAdd/TeamAdd';
import Team from './Team'; import Team from './Team';
class Teams extends Component { function Teams({ i18n }) {
constructor(props) { const [breadcrumbConfig, setBreadcrumbConfig] = useState({
super(props);
const { i18n } = props;
this.state = {
breadcrumbConfig: {
'/teams': i18n._(t`Teams`), '/teams': i18n._(t`Teams`),
'/teams/add': i18n._(t`Create New Team`), '/teams/add': i18n._(t`Create New Team`),
}, });
};
}
setBreadcrumbConfig = team => {
const { i18n } = this.props;
const buildBreadcrumbConfig = useCallback(
team => {
if (!team) { if (!team) {
return; return;
} }
const breadcrumbConfig = { setBreadcrumbConfig({
'/teams': i18n._(t`Teams`), '/teams': i18n._(t`Teams`),
'/teams/add': i18n._(t`Create New Team`), '/teams/add': i18n._(t`Create New Team`),
[`/teams/${team.id}`]: `${team.name}`, [`/teams/${team.id}`]: `${team.name}`,
@@ -39,41 +29,30 @@ class Teams extends Component {
[`/teams/${team.id}/details`]: i18n._(t`Details`), [`/teams/${team.id}/details`]: i18n._(t`Details`),
[`/teams/${team.id}/users`]: i18n._(t`Users`), [`/teams/${team.id}/users`]: i18n._(t`Users`),
[`/teams/${team.id}/access`]: i18n._(t`Access`), [`/teams/${team.id}/access`]: i18n._(t`Access`),
}; });
},
this.setState({ breadcrumbConfig }); [i18n]
}; );
render() {
const { match, history, location } = this.props;
const { breadcrumbConfig } = this.state;
return ( return (
<Fragment> <>
<Breadcrumbs breadcrumbConfig={breadcrumbConfig} /> <Breadcrumbs breadcrumbConfig={breadcrumbConfig} />
<Switch> <Switch>
<Route path={`${match.path}/add`} render={() => <TeamAdd />} /> <Route path="/teams/add">
<Route <TeamAdd />
path={`${match.path}/:id`} </Route>
render={() => ( <Route path="/teams/:id">
<Team setBreadcrumb={buildBreadcrumbConfig} />
</Route>
<Route path="/teams">
<Config> <Config>
{({ me }) => ( {({ me }) => <TeamList path="/teams" me={me || {}} />}
<Team
history={history}
location={location}
setBreadcrumb={this.setBreadcrumbConfig}
me={me || {}}
/>
)}
</Config> </Config>
)} </Route>
/>
<Route path={`${match.path}`} render={() => <TeamsList />} />
</Switch> </Switch>
</Fragment> </>
); );
} }
}
export { Teams as _Teams }; export { Teams as _Teams };
export default withI18n()(withRouter(Teams)); export default withI18n()(Teams);