Merge pull request #6335 from AlexSCorey/6316-TemplateUsesFunction

Moves template.jsx over to a functional component.

Reviewed-by: https://github.com/apps/softwarefactory-project-zuul
This commit is contained in:
softwarefactory-project-zuul[bot] 2020-03-20 14:54:49 +00:00 committed by GitHub
commit e17ff3e03a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 236 additions and 291 deletions

View File

@ -1,8 +1,18 @@
import React, { Component } from 'react';
import React, { useEffect, useCallback } from 'react';
import { t } from '@lingui/macro';
import { withI18n } from '@lingui/react';
import { Card, CardActions, PageSection } from '@patternfly/react-core';
import { Switch, Route, Redirect, withRouter, Link } from 'react-router-dom';
import {
Switch,
Route,
Redirect,
withRouter,
Link,
useLocation,
useParams,
useRouteMatch,
} from 'react-router-dom';
import useRequest from '@util/useRequest';
import { TabbedCardHeader } from '@components/Card';
import CardCloseButton from '@components/CardCloseButton';
@ -17,261 +27,195 @@ import JobTemplateEdit from './JobTemplateEdit';
import { JobTemplatesAPI, OrganizationsAPI } from '@api';
import TemplateSurvey from './TemplateSurvey';
class Template extends Component {
constructor(props) {
super(props);
function Template({ i18n, me, setBreadcrumb }) {
const location = useLocation();
const { id: templateId } = useParams();
const match = useRouteMatch();
this.state = {
contentError: null,
hasContentLoading: true,
template: null,
isNotifAdmin: false,
};
this.loadTemplate = this.loadTemplate.bind(this);
this.loadTemplateAndRoles = this.loadTemplateAndRoles.bind(this);
this.loadSchedules = this.loadSchedules.bind(this);
this.loadScheduleOptions = this.loadScheduleOptions.bind(this);
}
async componentDidMount() {
await this.loadTemplateAndRoles();
}
async componentDidUpdate(prevProps) {
const { location } = this.props;
if (location !== prevProps.location) {
await this.loadTemplate();
}
}
async loadTemplateAndRoles() {
const { match, setBreadcrumb } = this.props;
const id = parseInt(match.params.id, 10);
this.setState({ contentError: null, hasContentLoading: true });
try {
const {
result: { isNotifAdmin, template },
isLoading: hasRolesandTemplateLoading,
error: rolesAndTemplateError,
request: loadTemplateAndRoles,
} = useRequest(
useCallback(async () => {
const [{ data }, notifAdminRes] = await Promise.all([
JobTemplatesAPI.readDetail(id),
JobTemplatesAPI.readDetail(templateId),
OrganizationsAPI.read({
page_size: 1,
role_level: 'notification_admin_role',
}),
]);
setBreadcrumb(data);
this.setState({
return {
template: data,
isNotifAdmin: notifAdminRes.data.results.length > 0,
});
} catch (err) {
this.setState({ contentError: err });
} finally {
this.setState({ hasContentLoading: false });
}
}
};
}, [setBreadcrumb, templateId]),
{ isNotifAdmin: false, template: null }
);
useEffect(() => {
loadTemplateAndRoles();
}, [loadTemplateAndRoles]);
async loadTemplate() {
const { setBreadcrumb, match } = this.props;
const { id } = match.params;
const loadScheduleOptions = () => {
return JobTemplatesAPI.readScheduleOptions(templateId);
};
this.setState({ contentError: null, hasContentLoading: true });
try {
const { data } = await JobTemplatesAPI.readDetail(id);
setBreadcrumb(data);
this.setState({ template: data });
} catch (err) {
this.setState({ contentError: err });
} finally {
this.setState({ hasContentLoading: false });
}
}
const loadSchedules = params => {
return JobTemplatesAPI.readSchedules(templateId, params);
};
loadScheduleOptions() {
const { template } = this.state;
return JobTemplatesAPI.readScheduleOptions(template.id);
}
const canSeeNotificationsTab = me.is_system_auditor || isNotifAdmin;
loadSchedules(params) {
const { template } = this.state;
return JobTemplatesAPI.readSchedules(template.id, params);
}
const tabsArray = [
{ name: i18n._(t`Details`), link: `${match.url}/details` },
{ name: i18n._(t`Access`), link: `${match.url}/access` },
];
render() {
const { i18n, location, match, me, setBreadcrumb } = this.props;
const {
contentError,
hasContentLoading,
isNotifAdmin,
template,
} = this.state;
const canSeeNotificationsTab = me.is_system_auditor || isNotifAdmin;
const tabsArray = [
{ name: i18n._(t`Details`), link: `${match.url}/details` },
{ name: i18n._(t`Access`), link: `${match.url}/access` },
];
if (canSeeNotificationsTab) {
tabsArray.push({
name: i18n._(t`Notifications`),
link: `${match.url}/notifications`,
});
}
if (template) {
tabsArray.push({
name: i18n._(t`Schedules`),
link: `${match.url}/schedules`,
});
}
tabsArray.push(
{
name: i18n._(t`Completed Jobs`),
link: `${match.url}/completed_jobs`,
},
{
name: i18n._(t`Survey`),
link: `${match.url}/survey`,
}
);
tabsArray.forEach((tab, n) => {
tab.id = n;
if (canSeeNotificationsTab) {
tabsArray.push({
name: i18n._(t`Notifications`),
link: `${match.url}/notifications`,
});
}
let cardHeader = (
<TabbedCardHeader>
<RoutedTabs tabsArray={tabsArray} />
<CardActions>
<CardCloseButton linkTo="/templates" />
</CardActions>
</TabbedCardHeader>
);
if (template) {
tabsArray.push({
name: i18n._(t`Schedules`),
link: `${match.url}/schedules`,
});
}
if (
location.pathname.endsWith('edit') ||
location.pathname.includes('schedules/')
) {
cardHeader = null;
tabsArray.push(
{
name: i18n._(t`Completed Jobs`),
link: `${match.url}/completed_jobs`,
},
{
name: i18n._(t`Survey`),
link: `${match.url}/survey`,
}
);
if (!hasContentLoading && contentError) {
return (
<PageSection>
<Card>
<ContentError error={contentError}>
{contentError.response.status === 404 && (
<span>
{i18n._(`Template not found.`)}{' '}
<Link to="/templates">{i18n._(`View all Templates.`)}</Link>
</span>
)}
</ContentError>
</Card>
</PageSection>
);
}
tabsArray.forEach((tab, n) => {
tab.id = n;
});
let cardHeader = (
<TabbedCardHeader>
<RoutedTabs tabsArray={tabsArray} />
<CardActions>
<CardCloseButton linkTo="/templates" />
</CardActions>
</TabbedCardHeader>
);
if (
location.pathname.endsWith('edit') ||
location.pathname.includes('schedules/')
) {
cardHeader = null;
}
const contentError = rolesAndTemplateError;
if (!hasRolesandTemplateLoading && contentError) {
return (
<PageSection>
<Card>
{cardHeader}
<Switch>
<Redirect
from="/templates/:templateType/:id"
to="/templates/:templateType/:id/details"
exact
/>
{template && (
<Route
key="details"
path="/templates/:templateType/:id/details"
render={() => (
<JobTemplateDetail
hasTemplateLoading={hasContentLoading}
template={template}
/>
)}
/>
<ContentError error={contentError}>
{contentError.response.status === 404 && (
<span>
{i18n._(`Template not found.`)}{' '}
<Link to="/templates">{i18n._(`View all Templates.`)}</Link>
</span>
)}
{template && (
<Route
key="edit"
path="/templates/:templateType/:id/edit"
render={() => <JobTemplateEdit template={template} />}
/>
)}
{template && (
<Route
key="access"
path="/templates/:templateType/:id/access"
render={() => (
<ResourceAccessList
resource={template}
apiModel={JobTemplatesAPI}
/>
)}
/>
)}
{template && (
<Route
key="schedules"
path="/templates/:templateType/:id/schedules"
render={() => (
<Schedules
setBreadcrumb={setBreadcrumb}
unifiedJobTemplate={template}
loadSchedules={this.loadSchedules}
loadScheduleOptions={this.loadScheduleOptions}
/>
)}
/>
)}
{canSeeNotificationsTab && (
<Route
path="/templates/:templateType/:id/notifications"
render={() => (
<NotificationList
id={Number(match.params.id)}
canToggleNotifications={isNotifAdmin}
apiModel={JobTemplatesAPI}
/>
)}
/>
)}
{template?.id && (
<Route path="/templates/:templateType/:id/completed_jobs">
<JobList defaultParams={{ job__job_template: template.id }} />
</Route>
)}
{template && (
<Route path="/templates/:templateType/:id/survey">
<TemplateSurvey template={template} />
</Route>
)}
<Route
key="not-found"
path="*"
render={() =>
!hasContentLoading && (
<ContentError isNotFound>
{match.params.id && (
<Link
to={`/templates/${match.params.templateType}/${match.params.id}/details`}
>
{i18n._(`View Template Details`)}
</Link>
)}
</ContentError>
)
}
/>
</Switch>
</ContentError>
</Card>
</PageSection>
);
}
return (
<PageSection>
<Card>
{cardHeader}
<Switch>
<Redirect
from="/templates/:templateType/:id"
to="/templates/:templateType/:id/details"
exact
/>
{template && (
<Route key="details" path="/templates/:templateType/:id/details">
<JobTemplateDetail
hasTemplateLoading={hasRolesandTemplateLoading}
template={template}
/>
</Route>
)}
{template && (
<Route key="edit" path="/templates/:templateType/:id/edit">
<JobTemplateEdit template={template} />
</Route>
)}
{template && (
<Route key="access" path="/templates/:templateType/:id/access">
<ResourceAccessList
resource={template}
apiModel={JobTemplatesAPI}
/>
</Route>
)}
{template && (
<Route
key="schedules"
path="/templates/:templateType/:id/schedules"
>
<Schedules
setBreadcrumb={setBreadcrumb}
unifiedJobTemplate={template}
loadSchedules={loadSchedules}
loadScheduleOptions={loadScheduleOptions}
/>
</Route>
)}
{canSeeNotificationsTab && (
<Route path="/templates/:templateType/:id/notifications">
<NotificationList
id={Number(templateId)}
canToggleNotifications={isNotifAdmin}
apiModel={JobTemplatesAPI}
/>
</Route>
)}
{template?.id && (
<Route path="/templates/:templateType/:id/completed_jobs">
<JobList defaultParams={{ job__job_template: template.id }} />
</Route>
)}
{template && (
<Route path="/templates/:templateType/:id/survey">
<TemplateSurvey template={template} />
</Route>
)}
{!hasRolesandTemplateLoading && (
<Route key="not-found" path="*">
<ContentError isNotFound>
{match.params.id && (
<Link
to={`/templates/${match.params.templateType}/${match.params.id}/details`}
>
{i18n._(`View Template Details`)}
</Link>
)}
</ContentError>
</Route>
)}
</Switch>
</Card>
</PageSection>
);
}
export { Template as _Template };

View File

@ -1,63 +1,57 @@
import React from 'react';
import { createMemoryHistory } from 'history';
import { JobTemplatesAPI, OrganizationsAPI } from '@api';
import { act } from 'react-dom/test-utils';
import { mountWithContexts, waitForElement } from '@testUtils/enzymeHelpers';
import Template, { _Template } from './Template';
import Template from './Template';
import mockJobTemplateData from './shared/data.job_template.json';
jest.mock('@api');
JobTemplatesAPI.readDetail.mockResolvedValue({
data: mockJobTemplateData,
});
OrganizationsAPI.read.mockResolvedValue({
data: {
count: 1,
next: null,
previous: null,
results: [
{
id: 1,
},
],
},
});
jest.mock('@api/models/JobTemplates');
jest.mock('@api/models/Organizations');
const mockMe = {
is_super_user: true,
is_system_auditor: false,
};
describe('<Template />', () => {
test('initially renders succesfully', () => {
mountWithContexts(<Template setBreadcrumb={() => {}} me={mockMe} />);
beforeEach(() => {
JobTemplatesAPI.readDetail.mockResolvedValue({
data: mockJobTemplateData,
});
OrganizationsAPI.read.mockResolvedValue({
data: {
count: 1,
next: null,
previous: null,
results: [
{
id: 1,
},
],
},
});
});
test('When component mounts API is called and the response is put in state', async done => {
const loadTemplateAndRoles = jest.spyOn(
_Template.prototype,
'loadTemplateAndRoles'
);
const wrapper = mountWithContexts(
<Template setBreadcrumb={() => {}} me={mockMe} />
);
await waitForElement(
wrapper,
'Template',
el => el.state('hasContentLoading') === true
);
expect(loadTemplateAndRoles).toHaveBeenCalled();
await waitForElement(
wrapper,
'Template',
el => el.state('hasContentLoading') === true
);
done();
test('initially renders succesfully', async () => {
await act(async () => {
mountWithContexts(<Template setBreadcrumb={() => {}} me={mockMe} />);
});
});
test('When component mounts API is called and the response is put in state', async () => {
await act(async () => {
mountWithContexts(<Template setBreadcrumb={() => {}} me={mockMe} />);
});
expect(JobTemplatesAPI.readDetail).toBeCalled();
expect(OrganizationsAPI.read).toBeCalled();
});
test('notifications tab shown for admins', async done => {
const wrapper = mountWithContexts(
<Template setBreadcrumb={() => {}} me={mockMe} />
);
let wrapper;
await act(async () => {
wrapper = mountWithContexts(
<Template setBreadcrumb={() => {}} me={mockMe} />
);
});
const tabs = await waitForElement(
wrapper,
@ -77,9 +71,12 @@ describe('<Template />', () => {
},
});
const wrapper = mountWithContexts(
<Template setBreadcrumb={() => {}} me={mockMe} />
);
let wrapper;
await act(async () => {
wrapper = mountWithContexts(
<Template setBreadcrumb={() => {}} me={mockMe} />
);
});
const tabs = await waitForElement(
wrapper,
'.pf-c-tabs__item',
@ -93,24 +90,28 @@ describe('<Template />', () => {
const history = createMemoryHistory({
initialEntries: ['/templates/job_template/1/foobar'],
});
const wrapper = mountWithContexts(
<Template setBreadcrumb={() => {}} me={mockMe} />,
{
context: {
router: {
history,
route: {
location: history.location,
match: {
params: { id: 1 },
url: '/templates/job_template/1/foobar',
path: '/templates/job_template/1/foobar',
let wrapper;
await act(async () => {
wrapper = mountWithContexts(
<Template setBreadcrumb={() => {}} me={mockMe} />,
{
context: {
router: {
history,
route: {
location: history.location,
match: {
params: { id: 1 },
url: '/templates/job_template/1/foobar',
path: '/templates/job_template/1/foobar',
},
},
},
},
},
}
);
}
);
});
await waitForElement(wrapper, 'ContentError', el => el.length === 1);
});
});