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,148 +18,110 @@ 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, try {
contentError: null, const { data } = await TeamsAPI.readDetail(id);
isInitialized: false, setBreadcrumb(data);
}; setTeam(data);
this.loadTeam = this.loadTeam.bind(this); } catch (error) {
setContentError(error);
} finally {
setHasContentLoading(false);
}
})();
}, [id, setBreadcrumb]);
const tabsArray = [
{ name: i18n._(t`Details`), link: `/teams/${id}/details`, id: 0 },
{ name: i18n._(t`Users`), link: `/teams/${id}/users`, id: 1 },
{ name: i18n._(t`Access`), link: `/teams/${id}/access`, id: 2 },
];
let cardHeader = (
<TabbedCardHeader>
<RoutedTabs tabsArray={tabsArray} />
<CardCloseButton linkTo="/teams" />
</TabbedCardHeader>
);
if (location.pathname.endsWith('edit')) {
cardHeader = null;
} }
async componentDidMount() { if (!hasContentLoading && contentError) {
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 {
const { data } = await TeamsAPI.readDetail(id);
setBreadcrumb(data);
this.setState({ team: data });
} catch (err) {
this.setState({ contentError: err });
} finally {
this.setState({ hasContentLoading: false });
}
}
render() {
const { location, match, i18n } = this.props;
const { team, contentError, hasContentLoading, isInitialized } = this.state;
const tabsArray = [
{ name: i18n._(t`Details`), link: `${match.url}/details`, id: 0 },
{ name: i18n._(t`Users`), link: `${match.url}/users`, id: 1 },
{ name: i18n._(t`Access`), link: `${match.url}/access`, id: 2 },
];
let cardHeader = (
<TabbedCardHeader>
<RoutedTabs tabsArray={tabsArray} />
<CardCloseButton linkTo="/teams" />
</TabbedCardHeader>
);
if (!isInitialized) {
cardHeader = null;
}
if (location.pathname.endsWith('edit')) {
cardHeader = null;
}
if (!hasContentLoading && contentError) {
return (
<PageSection>
<Card className="awx-c-card">
<ContentError error={contentError}>
{contentError.response.status === 404 && (
<span>
{i18n._(`Team not found.`)}{' '}
<Link to="/teams">{i18n._(`View all Teams.`)}</Link>
</span>
)}
</ContentError>
</Card>
</PageSection>
);
}
return ( return (
<PageSection> <PageSection>
<Card className="awx-c-card"> <Card className="awx-c-card">
{cardHeader} <ContentError error={contentError}>
<Switch> {contentError.response.status === 404 && (
<Redirect from="/teams/:id" to="/teams/:id/details" exact /> <span>
{team && ( {i18n._(`Team not found.`)}{' '}
<Route <Link to="/teams">{i18n._(`View all Teams.`)}</Link>
path="/teams/:id/edit" </span>
render={() => <TeamEdit team={team} />}
/>
)} )}
{team && ( </ContentError>
<Route
path="/teams/:id/details"
render={() => <TeamDetail team={team} />}
/>
)}
{team && (
<Route
path="/teams/:id/users"
render={() => <span>Coming soon :)</span>}
/>
)}
{team && (
<Route
path="/teams/:id/access"
render={() => <span>Coming soon :)</span>}
/>
)}
<Route
key="not-found"
path="*"
render={() =>
!hasContentLoading && (
<ContentError isNotFound>
{match.params.id && (
<Link to={`/teams/${match.params.id}/details`}>
{i18n._(`View Team Details`)}
</Link>
)}
</ContentError>
)
}
/>
,
</Switch>
</Card> </Card>
</PageSection> </PageSection>
); );
} }
return (
<PageSection>
<Card className="awx-c-card">
{cardHeader}
<Switch>
<Redirect from="/teams/:id" to="/teams/:id/details" exact />
{team && (
<Route path="/teams/:id/details">
<TeamDetail team={team} />
</Route>
)}
{team && (
<Route
path="/teams/:id/edit"
render={() => <TeamEdit team={team} />}
/>
)}
{team && (
<Route
path="/teams/:id/users"
render={() => <span>Coming soon :)</span>}
/>
)}
{team && (
<Route
path="/teams/:id/access"
render={() => <span>Coming soon :)</span>}
/>
)}
<Route
key="not-found"
path="*"
render={() =>
!hasContentLoading && (
<ContentError isNotFound>
{id && (
<Link to={`/teams/${id}/details`}>
{i18n._(`View Team Details`)}
</Link>
)}
</ContentError>
)
}
/>
</Switch>
</Card>
</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,34 +35,46 @@ 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 () => {
<Team setBreadcrumb={() => {}} me={mockMe} />, wrapper = mountWithContexts(
{ <Team setBreadcrumb={() => {}} me={mockMe} />,
context: { {
router: { context: {
history, router: {
route: { history,
location: history.location, route: {
match: { location: history.location,
params: { id: 1 }, match: {
url: '/teams/1/foobar', params: { id: 1 },
path: '/teams/1/foobar', url: '/teams/1/foobar',
path: '/teams/1/foobar',
},
}, },
}, },
}, },
}, }
} );
); });
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,79 +1,58 @@
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); '/teams': i18n._(t`Teams`),
'/teams/add': i18n._(t`Create New Team`),
});
const { i18n } = props; const buildBreadcrumbConfig = useCallback(
team => {
if (!team) {
return;
}
this.state = { setBreadcrumbConfig({
breadcrumbConfig: {
'/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}/edit`]: i18n._(t`Edit Details`),
} [`/teams/${team.id}/details`]: i18n._(t`Details`),
[`/teams/${team.id}/users`]: i18n._(t`Users`),
[`/teams/${team.id}/access`]: i18n._(t`Access`),
});
},
[i18n]
);
setBreadcrumbConfig = team => { return (
const { i18n } = this.props; <>
<Breadcrumbs breadcrumbConfig={breadcrumbConfig} />
if (!team) { <Switch>
return; <Route path="/teams/add">
} <TeamAdd />
</Route>
const breadcrumbConfig = { <Route path="/teams/:id">
'/teams': i18n._(t`Teams`), <Team setBreadcrumb={buildBreadcrumbConfig} />
'/teams/add': i18n._(t`Create New Team`), </Route>
[`/teams/${team.id}`]: `${team.name}`, <Route path="/teams">
[`/teams/${team.id}/edit`]: i18n._(t`Edit Details`), <Config>
[`/teams/${team.id}/details`]: i18n._(t`Details`), {({ me }) => <TeamList path="/teams" me={me || {}} />}
[`/teams/${team.id}/users`]: i18n._(t`Users`), </Config>
[`/teams/${team.id}/access`]: i18n._(t`Access`), </Route>
}; </Switch>
</>
this.setState({ breadcrumbConfig }); );
};
render() {
const { match, history, location } = this.props;
const { breadcrumbConfig } = this.state;
return (
<Fragment>
<Breadcrumbs breadcrumbConfig={breadcrumbConfig} />
<Switch>
<Route path={`${match.path}/add`} render={() => <TeamAdd />} />
<Route
path={`${match.path}/:id`}
render={() => (
<Config>
{({ me }) => (
<Team
history={history}
location={location}
setBreadcrumb={this.setBreadcrumbConfig}
me={me || {}}
/>
)}
</Config>
)}
/>
<Route path={`${match.path}`} render={() => <TeamsList />} />
</Switch>
</Fragment>
);
}
} }
export { Teams as _Teams }; export { Teams as _Teams };
export default withI18n()(withRouter(Teams)); export default withI18n()(Teams);