зеркало из https://github.com/mozilla/treeherder.git
Bug 1571404 - Set assignees for alert summaries
This commit is contained in:
Родитель
347628d91d
Коммит
53c639c166
|
@ -3,9 +3,9 @@ import {
|
||||||
render,
|
render,
|
||||||
cleanup,
|
cleanup,
|
||||||
fireEvent,
|
fireEvent,
|
||||||
|
wait,
|
||||||
waitForElement,
|
waitForElement,
|
||||||
waitForElementToBeRemoved,
|
waitForElementToBeRemoved,
|
||||||
wait,
|
|
||||||
} from '@testing-library/react';
|
} from '@testing-library/react';
|
||||||
|
|
||||||
import AlertsViewControls from '../../../ui/perfherder/alerts/AlertsViewControls';
|
import AlertsViewControls from '../../../ui/perfherder/alerts/AlertsViewControls';
|
||||||
|
@ -14,9 +14,9 @@ import { summaryStatusMap } from '../../../ui/perfherder/constants';
|
||||||
import repos from '../mock/repositories';
|
import repos from '../mock/repositories';
|
||||||
|
|
||||||
const testUser = {
|
const testUser = {
|
||||||
username: 'test user',
|
username: 'mozilla-ldap/test_user@mozilla.com',
|
||||||
is_superuser: false,
|
is_superuser: false,
|
||||||
is_staff: true,
|
isStaff: true,
|
||||||
email: 'test_user@mozilla.com',
|
email: 'test_user@mozilla.com',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -95,6 +95,8 @@ const testAlertSummaries = [
|
||||||
revision: '930f0f51b681aea2a5e915a2770f80a9914ed3df',
|
revision: '930f0f51b681aea2a5e915a2770f80a9914ed3df',
|
||||||
push_timestamp: 1558111832,
|
push_timestamp: 1558111832,
|
||||||
prev_push_revision: '76e3a842e496d78a80cd547b7bf94f041f9bc612',
|
prev_push_revision: '76e3a842e496d78a80cd547b7bf94f041f9bc612',
|
||||||
|
assignee_username: null,
|
||||||
|
assignee_email: null,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 20239,
|
id: 20239,
|
||||||
|
@ -170,6 +172,8 @@ const testAlertSummaries = [
|
||||||
revision: 'd4a9b4dd03ca5c3db2bd10e8097d9817435ba37d',
|
revision: 'd4a9b4dd03ca5c3db2bd10e8097d9817435ba37d',
|
||||||
push_timestamp: 1558583128,
|
push_timestamp: 1558583128,
|
||||||
prev_push_revision: 'c8e9b6a81194dff2d37b4f67d23a419fd4587e49',
|
prev_push_revision: 'c8e9b6a81194dff2d37b4f67d23a419fd4587e49',
|
||||||
|
assignee_username: 'mozilla-ldap/test_user@mozilla.com',
|
||||||
|
assignee_email: 'test_user@mozilla.com',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -213,6 +217,11 @@ const mockModifyAlert = {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
const mockUpdateAlertSummary = (alertSummaryId, params) => ({
|
||||||
|
failureStatus: null,
|
||||||
|
});
|
||||||
|
|
||||||
const alertsViewControls = () =>
|
const alertsViewControls = () =>
|
||||||
render(
|
render(
|
||||||
<AlertsViewControls
|
<AlertsViewControls
|
||||||
|
@ -230,6 +239,9 @@ const alertsViewControls = () =>
|
||||||
updateViewState={() => {}}
|
updateViewState={() => {}}
|
||||||
user={testUser}
|
user={testUser}
|
||||||
modifyAlert={(alert, params) => mockModifyAlert.update(alert, params)}
|
modifyAlert={(alert, params) => mockModifyAlert.update(alert, params)}
|
||||||
|
updateAlertSummary={() =>
|
||||||
|
Promise.resolve({ failureStatus: false, data: 'alert summary data' })
|
||||||
|
}
|
||||||
projects={repos}
|
projects={repos}
|
||||||
location={{
|
location={{
|
||||||
pathname: '/alerts',
|
pathname: '/alerts',
|
||||||
|
@ -429,5 +441,107 @@ test('selecting the alert summary checkbox then deselecting one alert only updat
|
||||||
modifyAlertSpy.mockClear();
|
modifyAlertSpy.mockClear();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("display of alert summaries's assignee badge", async () => {
|
||||||
|
const { getAllByTitle, getAllByText } = alertsViewControls();
|
||||||
|
|
||||||
|
const ownershipBadges = getAllByTitle('Click to change assignee');
|
||||||
|
const takeButtons = getAllByText('Take');
|
||||||
|
|
||||||
|
// summary with no assignee defaults to "Unassigned" badge &
|
||||||
|
// displays the 'Take' button
|
||||||
|
expect(ownershipBadges[0]).toHaveTextContent('Unassigned');
|
||||||
|
expect(takeButtons).toHaveLength(1);
|
||||||
|
|
||||||
|
// summary with assignee displays username extracted from email &
|
||||||
|
// hides the 'Take' button
|
||||||
|
expect(ownershipBadges[1]).toHaveTextContent('test_user');
|
||||||
|
});
|
||||||
|
|
||||||
|
test("'Take' button hides when clicking on 'Unassigned' badge", async () => {
|
||||||
|
const {
|
||||||
|
getByText,
|
||||||
|
queryByText,
|
||||||
|
queryByPlaceholderText,
|
||||||
|
} = alertsViewControls();
|
||||||
|
|
||||||
|
const unassignedBadge = await waitForElement(() => getByText('Unassigned'));
|
||||||
|
|
||||||
|
await fireEvent.click(unassignedBadge);
|
||||||
|
expect(queryByText('Take')).not.toBeInTheDocument();
|
||||||
|
// and the placeholder nicely shows up
|
||||||
|
expect(queryByPlaceholderText('nobody@mozilla.org')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('setting an assignee on unassigned alert summary updates the badge accordingly', async () => {
|
||||||
|
const { getByText, getByPlaceholderText } = alertsViewControls();
|
||||||
|
|
||||||
|
const unassignedBadge = await waitForElement(() => getByText('Unassigned'));
|
||||||
|
|
||||||
|
fireEvent.click(unassignedBadge);
|
||||||
|
const inputField = await waitForElement(() =>
|
||||||
|
getByPlaceholderText('nobody@mozilla.org'),
|
||||||
|
);
|
||||||
|
fireEvent.change(inputField, {
|
||||||
|
target: { value: 'mozilla-ldap/test_assignee@mozilla.com' },
|
||||||
|
});
|
||||||
|
// pressing 'Enter' has some issues on react-testing-library;
|
||||||
|
// found workaround on https://github.com/testing-library/react-testing-library/issues/269
|
||||||
|
fireEvent.keyPress(inputField, { key: 'Enter', keyCode: 13 });
|
||||||
|
|
||||||
|
// ensure this updated the assignee
|
||||||
|
await waitForElement(() => getByText('test_assignee'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('setting an assignee on an already assigned summary is possible', async () => {
|
||||||
|
const { getByText, getByDisplayValue } = alertsViewControls();
|
||||||
|
|
||||||
|
const unassignedBadge = await waitForElement(() => getByText('test_user'));
|
||||||
|
|
||||||
|
fireEvent.click(unassignedBadge);
|
||||||
|
const inputField = await waitForElement(() =>
|
||||||
|
getByDisplayValue('mozilla-ldap/test_user@mozilla.com'),
|
||||||
|
);
|
||||||
|
fireEvent.change(inputField, {
|
||||||
|
target: { value: 'mozilla-ldap/test_another_user@mozilla.com' },
|
||||||
|
});
|
||||||
|
// pressing 'Enter' has some issues on react-testing-library;
|
||||||
|
// found workaround on https://github.com/testing-library/react-testing-library/issues/269
|
||||||
|
fireEvent.keyPress(inputField, { key: 'Enter', keyCode: 13 });
|
||||||
|
|
||||||
|
// ensure this updated the assignee
|
||||||
|
await waitForElement(() => getByText('test_another_user'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test("'Escape' from partially editted assignee does not update original assignee", async () => {
|
||||||
|
const { getByText, getByDisplayValue } = alertsViewControls();
|
||||||
|
|
||||||
|
const unassignedBadge = await waitForElement(() => getByText('test_user'));
|
||||||
|
|
||||||
|
fireEvent.click(unassignedBadge);
|
||||||
|
const inputField = await waitForElement(() =>
|
||||||
|
getByDisplayValue('mozilla-ldap/test_user@mozilla.com'),
|
||||||
|
);
|
||||||
|
fireEvent.change(inputField, {
|
||||||
|
target: { value: 'mozilla-ldap/test_another_' },
|
||||||
|
});
|
||||||
|
fireEvent.keyDown(inputField, { key: 'Escape' });
|
||||||
|
|
||||||
|
// ensure assignee wasn't updated
|
||||||
|
await waitForElement(() => getByText('test_user'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Clicking on 'Take' prefills with logged in user", async () => {
|
||||||
|
const { getByText, getByDisplayValue } = alertsViewControls();
|
||||||
|
|
||||||
|
const takeButton = getByText('Take');
|
||||||
|
|
||||||
|
fireEvent.click(takeButton);
|
||||||
|
|
||||||
|
// ensure it preffiled input field
|
||||||
|
await waitForElement(() =>
|
||||||
|
getByDisplayValue('mozilla-ldap/test_user@mozilla.com'),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
// TODO should write tests for alert summary dropdown menu actions performed in StatusDropdown
|
// TODO should write tests for alert summary dropdown menu actions performed in StatusDropdown
|
||||||
// (adding notes or marking as 'fixed', etc)
|
// (adding notes or marking as 'fixed', etc)
|
||||||
|
|
|
@ -5,6 +5,9 @@ import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownItem,
|
DropdownItem,
|
||||||
DropdownToggle,
|
DropdownToggle,
|
||||||
|
Container,
|
||||||
|
Row,
|
||||||
|
Col,
|
||||||
} from 'reactstrap';
|
} from 'reactstrap';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
import { faExternalLinkAlt } from '@fortawesome/free-solid-svg-icons';
|
import { faExternalLinkAlt } from '@fortawesome/free-solid-svg-icons';
|
||||||
|
@ -13,7 +16,15 @@ import moment from 'moment';
|
||||||
import { getTitle } from '../helpers';
|
import { getTitle } from '../helpers';
|
||||||
import { getJobsUrl } from '../../helpers/url';
|
import { getJobsUrl } from '../../helpers/url';
|
||||||
|
|
||||||
const AlertHeader = ({ alertSummary, repoModel, issueTrackers }) => {
|
import Assignee from './Assignee';
|
||||||
|
|
||||||
|
const AlertHeader = ({
|
||||||
|
alertSummary,
|
||||||
|
repoModel,
|
||||||
|
issueTrackers,
|
||||||
|
user,
|
||||||
|
updateAssignee,
|
||||||
|
}) => {
|
||||||
const getIssueTrackerUrl = () => {
|
const getIssueTrackerUrl = () => {
|
||||||
const { issueTrackerUrl } = issueTrackers.find(
|
const { issueTrackerUrl } = issueTrackers.find(
|
||||||
tracker => tracker.id === alertSummary.issue_tracker,
|
tracker => tracker.id === alertSummary.issue_tracker,
|
||||||
|
@ -25,62 +36,65 @@ const AlertHeader = ({ alertSummary, repoModel, issueTrackers }) => {
|
||||||
: '';
|
: '';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="pl-2">
|
<Container>
|
||||||
<a
|
<Row>
|
||||||
className="text-dark font-weight-bold align-middle"
|
<a
|
||||||
href={`#/alerts?id=${alertSummary.id}`}
|
className="text-dark font-weight-bold align-middle"
|
||||||
id={`alert summary ${alertSummary.id.toString()} title`}
|
href={`#/alerts?id=${alertSummary.id}`}
|
||||||
data-testid={`alert summary ${alertSummary.id.toString()} title`}
|
id={`alert summary ${alertSummary.id.toString()} title`}
|
||||||
>
|
data-testid={`alert summary ${alertSummary.id.toString()} title`}
|
||||||
Alert #{alertSummary.id} - {alertSummary.repository} -{' '}
|
>
|
||||||
{getTitle(alertSummary)}{' '}
|
Alert #{alertSummary.id} - {alertSummary.repository} -{' '}
|
||||||
<FontAwesomeIcon
|
{getTitle(alertSummary)}{' '}
|
||||||
icon={faExternalLinkAlt}
|
<FontAwesomeIcon
|
||||||
className="icon-superscript"
|
icon={faExternalLinkAlt}
|
||||||
/>
|
className="icon-superscript"
|
||||||
</a>
|
/>
|
||||||
<br />
|
</a>
|
||||||
<span className="font-weight-normal">
|
</Row>
|
||||||
<span className="align-middle">{`${moment(
|
<Row className="font-weight-normal">
|
||||||
|
<Col className="p-0" xs="auto">{`${moment(
|
||||||
alertSummary.push_timestamp * 1000,
|
alertSummary.push_timestamp * 1000,
|
||||||
).format('ddd MMM D, HH:mm:ss')} · `}</span>
|
).format('ddd MMM D, HH:mm:ss')} ·`}</Col>
|
||||||
<UncontrolledDropdown tag="span">
|
<Col className="p-0" xs="auto">
|
||||||
<DropdownToggle
|
<UncontrolledDropdown tag="span">
|
||||||
className="btn-link text-info p-0"
|
<DropdownToggle
|
||||||
color="transparent"
|
className="btn-link text-info p-0"
|
||||||
caret
|
color="transparent"
|
||||||
>
|
caret
|
||||||
{alertSummary.revision.slice(0, 12)}
|
|
||||||
</DropdownToggle>
|
|
||||||
<DropdownMenu>
|
|
||||||
<a
|
|
||||||
className="text-dark"
|
|
||||||
href={getJobsUrl({
|
|
||||||
repo: alertSummary.repository,
|
|
||||||
fromchange: alertSummary.prev_push_revision,
|
|
||||||
tochange: alertSummary.revision,
|
|
||||||
})}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
>
|
||||||
<DropdownItem>Jobs</DropdownItem>
|
{alertSummary.revision.slice(0, 12)}
|
||||||
</a>
|
</DropdownToggle>
|
||||||
<a
|
<DropdownMenu>
|
||||||
className="text-dark"
|
<a
|
||||||
href={repoModel.getPushLogRangeHref({
|
className="text-dark"
|
||||||
fromchange: alertSummary.prev_push_revision,
|
href={getJobsUrl({
|
||||||
tochange: alertSummary.revision,
|
repo: alertSummary.repository,
|
||||||
})}
|
fromchange: alertSummary.prev_push_revision,
|
||||||
target="_blank"
|
tochange: alertSummary.revision,
|
||||||
rel="noopener noreferrer"
|
})}
|
||||||
>
|
target="_blank"
|
||||||
<DropdownItem>Pushlog</DropdownItem>
|
rel="noopener noreferrer"
|
||||||
</a>
|
>
|
||||||
</DropdownMenu>
|
<DropdownItem>Jobs</DropdownItem>
|
||||||
</UncontrolledDropdown>
|
</a>
|
||||||
|
<a
|
||||||
|
className="text-dark"
|
||||||
|
href={repoModel.getPushLogRangeHref({
|
||||||
|
fromchange: alertSummary.prev_push_revision,
|
||||||
|
tochange: alertSummary.revision,
|
||||||
|
})}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
<DropdownItem>Pushlog</DropdownItem>
|
||||||
|
</a>
|
||||||
|
</DropdownMenu>
|
||||||
|
</UncontrolledDropdown>
|
||||||
|
<span>·</span>
|
||||||
|
</Col>
|
||||||
{bugNumber && (
|
{bugNumber && (
|
||||||
<span>
|
<Col className="p-0" xs="auto">
|
||||||
<span className="align-middle"> · </span>
|
|
||||||
{alertSummary.issue_tracker && issueTrackers.length > 0 ? (
|
{alertSummary.issue_tracker && issueTrackers.length > 0 ? (
|
||||||
<a
|
<a
|
||||||
className="text-info align-middle"
|
className="text-info align-middle"
|
||||||
|
@ -93,16 +107,25 @@ const AlertHeader = ({ alertSummary, repoModel, issueTrackers }) => {
|
||||||
) : (
|
) : (
|
||||||
{ bugNumber }
|
{ bugNumber }
|
||||||
)}
|
)}
|
||||||
</span>
|
<span>·</span>
|
||||||
|
</Col>
|
||||||
)}
|
)}
|
||||||
</span>
|
<Col className="p-0" xs="auto">
|
||||||
</div>
|
<Assignee
|
||||||
|
assigneeUsername={alertSummary.assignee_username}
|
||||||
|
updateAssignee={updateAssignee}
|
||||||
|
user={user}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</Container>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
AlertHeader.propTypes = {
|
AlertHeader.propTypes = {
|
||||||
alertSummary: PropTypes.shape({}).isRequired,
|
alertSummary: PropTypes.shape({}).isRequired,
|
||||||
repoModel: PropTypes.shape({}).isRequired,
|
repoModel: PropTypes.shape({}).isRequired,
|
||||||
|
user: PropTypes.shape({}).isRequired,
|
||||||
issueTrackers: PropTypes.arrayOf(PropTypes.shape({})),
|
issueTrackers: PropTypes.arrayOf(PropTypes.shape({})),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -9,7 +9,11 @@ import {
|
||||||
errorMessageClass,
|
errorMessageClass,
|
||||||
} from '../../helpers/constants';
|
} from '../../helpers/constants';
|
||||||
import RepositoryModel from '../../models/repository';
|
import RepositoryModel from '../../models/repository';
|
||||||
import { getInitializedAlerts, containsText } from '../helpers';
|
import {
|
||||||
|
getInitializedAlerts,
|
||||||
|
containsText,
|
||||||
|
updateAlertSummary,
|
||||||
|
} from '../helpers';
|
||||||
import TruncatedText from '../../shared/TruncatedText';
|
import TruncatedText from '../../shared/TruncatedText';
|
||||||
import ErrorBoundary from '../../shared/ErrorBoundary';
|
import ErrorBoundary from '../../shared/ErrorBoundary';
|
||||||
|
|
||||||
|
@ -119,6 +123,32 @@ export default class AlertTable extends React.Component {
|
||||||
this.setState({ filteredAlerts });
|
this.setState({ filteredAlerts });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
updateAssignee = async newAssigneeUsername => {
|
||||||
|
const {
|
||||||
|
updateAlertSummary,
|
||||||
|
updateViewState,
|
||||||
|
fetchAlertSummaries,
|
||||||
|
} = this.props;
|
||||||
|
const { alertSummary } = this.state;
|
||||||
|
|
||||||
|
const { data, failureStatus } = await updateAlertSummary(alertSummary.id, {
|
||||||
|
assignee_username: newAssigneeUsername,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!failureStatus) {
|
||||||
|
// now refresh UI, by syncing with backend
|
||||||
|
fetchAlertSummaries(alertSummary.id);
|
||||||
|
} else {
|
||||||
|
updateViewState({
|
||||||
|
errorMessages: [
|
||||||
|
`Failed to set new assignee "${newAssigneeUsername}". (${data})`,
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return { failureStatus };
|
||||||
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
user,
|
user,
|
||||||
|
@ -180,6 +210,8 @@ export default class AlertTable extends React.Component {
|
||||||
alertSummary={alertSummary}
|
alertSummary={alertSummary}
|
||||||
repoModel={repoModel}
|
repoModel={repoModel}
|
||||||
issueTrackers={issueTrackers}
|
issueTrackers={issueTrackers}
|
||||||
|
user={user}
|
||||||
|
updateAssignee={this.updateAssignee}
|
||||||
/>
|
/>
|
||||||
</Label>
|
</Label>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
@ -280,7 +312,7 @@ export default class AlertTable extends React.Component {
|
||||||
|
|
||||||
AlertTable.propTypes = {
|
AlertTable.propTypes = {
|
||||||
alertSummary: PropTypes.shape({}),
|
alertSummary: PropTypes.shape({}),
|
||||||
user: PropTypes.shape({}),
|
user: PropTypes.shape({}).isRequired,
|
||||||
alertSummaries: PropTypes.arrayOf(PropTypes.shape({})).isRequired,
|
alertSummaries: PropTypes.arrayOf(PropTypes.shape({})).isRequired,
|
||||||
issueTrackers: PropTypes.arrayOf(PropTypes.shape({})),
|
issueTrackers: PropTypes.arrayOf(PropTypes.shape({})),
|
||||||
optionCollectionMap: PropTypes.shape({}).isRequired,
|
optionCollectionMap: PropTypes.shape({}).isRequired,
|
||||||
|
@ -293,13 +325,16 @@ AlertTable.propTypes = {
|
||||||
updateViewState: PropTypes.func.isRequired,
|
updateViewState: PropTypes.func.isRequired,
|
||||||
bugTemplate: PropTypes.shape({}),
|
bugTemplate: PropTypes.shape({}),
|
||||||
modifyAlert: PropTypes.func,
|
modifyAlert: PropTypes.func,
|
||||||
|
updateAlertSummary: PropTypes.func,
|
||||||
projects: PropTypes.arrayOf(PropTypes.shape({})).isRequired,
|
projects: PropTypes.arrayOf(PropTypes.shape({})).isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
AlertTable.defaultProps = {
|
AlertTable.defaultProps = {
|
||||||
alertSummary: null,
|
alertSummary: null,
|
||||||
user: null,
|
|
||||||
issueTrackers: [],
|
issueTrackers: [],
|
||||||
bugTemplate: null,
|
bugTemplate: null,
|
||||||
modifyAlert: undefined,
|
modifyAlert: undefined,
|
||||||
|
// leverage dependency injection
|
||||||
|
// to improve code testability
|
||||||
|
updateAlertSummary,
|
||||||
};
|
};
|
||||||
|
|
|
@ -48,8 +48,14 @@ export default class AlertsViewControls extends React.Component {
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
const {
|
||||||
|
alertSummaries,
|
||||||
|
dropdownOptions,
|
||||||
|
fetchAlertSummaries,
|
||||||
|
user,
|
||||||
|
} = this.props;
|
||||||
const { hideImprovements, hideDownstream } = this.state;
|
const { hideImprovements, hideDownstream } = this.state;
|
||||||
const { dropdownOptions, alertSummaries } = this.props;
|
|
||||||
const alertFilters = [
|
const alertFilters = [
|
||||||
{
|
{
|
||||||
text: 'Hide improvements',
|
text: 'Hide improvements',
|
||||||
|
@ -78,7 +84,9 @@ export default class AlertsViewControls extends React.Component {
|
||||||
filters={this.state}
|
filters={this.state}
|
||||||
key={alertSummary.id}
|
key={alertSummary.id}
|
||||||
alertSummary={alertSummary}
|
alertSummary={alertSummary}
|
||||||
|
fetchAlertSummaries={fetchAlertSummaries}
|
||||||
{...this.props}
|
{...this.props}
|
||||||
|
user={user}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
|
@ -91,7 +99,9 @@ AlertsViewControls.propTypes = {
|
||||||
updateParams: PropTypes.func,
|
updateParams: PropTypes.func,
|
||||||
}).isRequired,
|
}).isRequired,
|
||||||
dropdownOptions: PropTypes.arrayOf(PropTypes.shape({})),
|
dropdownOptions: PropTypes.arrayOf(PropTypes.shape({})),
|
||||||
|
fetchAlertSummaries: PropTypes.func.isRequired,
|
||||||
alertSummaries: PropTypes.arrayOf(PropTypes.shape({})),
|
alertSummaries: PropTypes.arrayOf(PropTypes.shape({})),
|
||||||
|
user: PropTypes.shape({}).isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
AlertsViewControls.defaultProps = {
|
AlertsViewControls.defaultProps = {
|
||||||
|
|
|
@ -0,0 +1,145 @@
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { Button, Input, InputGroup } from 'reactstrap';
|
||||||
|
|
||||||
|
export default class Assignee extends React.Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
const { assigneeUsername } = props;
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
assigneeUsername,
|
||||||
|
inEditMode: false,
|
||||||
|
newAssigneeUsername: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
// React's onKeyPress isn't able to listen to 'Escape'.
|
||||||
|
// This is a workaround.
|
||||||
|
document.addEventListener('keydown', this.keydownListener);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
document.removeEventListener('keydown', this.keydownListener);
|
||||||
|
}
|
||||||
|
|
||||||
|
goToEditMode = () => {
|
||||||
|
const { user } = this.props;
|
||||||
|
const { assigneeUsername } = this.state;
|
||||||
|
|
||||||
|
if (user.isStaff) {
|
||||||
|
this.setState({
|
||||||
|
inEditMode: true,
|
||||||
|
// input prefills with this field, so
|
||||||
|
// we must have it prepared
|
||||||
|
newAssigneeUsername: assigneeUsername,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
editUsername = newAssigneeUsername => {
|
||||||
|
this.setState({ newAssigneeUsername });
|
||||||
|
};
|
||||||
|
|
||||||
|
pressedEnter = async event => {
|
||||||
|
if (event.key === 'Enter') {
|
||||||
|
const { updateAssignee } = this.props;
|
||||||
|
const newAssigneeUsername = event.target.value;
|
||||||
|
|
||||||
|
const { failureStatus } = await updateAssignee(newAssigneeUsername);
|
||||||
|
|
||||||
|
if (!failureStatus) {
|
||||||
|
this.setState({
|
||||||
|
assigneeUsername: newAssigneeUsername,
|
||||||
|
inEditMode: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
prefillWithLoggedInUsername = () => {
|
||||||
|
const { user } = this.props;
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
newAssigneeUsername: user.username,
|
||||||
|
inEditMode: true,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
keydownListener = event => {
|
||||||
|
if (event.key === 'Escape') {
|
||||||
|
this.setState({ inEditMode: false });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
extractNicknameAndPlaceholder = assigneeUsername => {
|
||||||
|
let nickname = 'Unassigned';
|
||||||
|
const placeholder = 'nobody@mozilla.org';
|
||||||
|
|
||||||
|
if (!assigneeUsername) {
|
||||||
|
return { nickname, placeholder };
|
||||||
|
}
|
||||||
|
|
||||||
|
const nicknameRegex = /\/(\w+)@/g;
|
||||||
|
// eslint-disable-next-line prefer-destructuring
|
||||||
|
nickname = nicknameRegex.exec(assigneeUsername)[1];
|
||||||
|
|
||||||
|
return { nickname, placeholder };
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { user } = this.props;
|
||||||
|
const { assigneeUsername, newAssigneeUsername, inEditMode } = this.state;
|
||||||
|
|
||||||
|
const { nickname, placeholder } = this.extractNicknameAndPlaceholder(
|
||||||
|
assigneeUsername,
|
||||||
|
);
|
||||||
|
|
||||||
|
return !inEditMode ? (
|
||||||
|
<React.Fragment>
|
||||||
|
<Button
|
||||||
|
className="ml-1"
|
||||||
|
color="outline-info"
|
||||||
|
size="xs"
|
||||||
|
onClick={this.goToEditMode}
|
||||||
|
title="Click to change assignee"
|
||||||
|
>
|
||||||
|
{nickname}
|
||||||
|
</Button>
|
||||||
|
{!assigneeUsername && (
|
||||||
|
<Button
|
||||||
|
className="ml-1"
|
||||||
|
size="xs"
|
||||||
|
disabled={!user.isStaff}
|
||||||
|
onClick={this.prefillWithLoggedInUsername}
|
||||||
|
>
|
||||||
|
Take
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</React.Fragment>
|
||||||
|
) : (
|
||||||
|
<InputGroup size="sm">
|
||||||
|
<Input
|
||||||
|
disabled={!user.isStaff}
|
||||||
|
placeholder={placeholder}
|
||||||
|
value={newAssigneeUsername}
|
||||||
|
aria-label="Set assignee"
|
||||||
|
onChange={event => this.editUsername(event.target.value)}
|
||||||
|
onKeyPress={event => this.pressedEnter(event)}
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
</InputGroup>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Assignee.propTypes = {
|
||||||
|
updateAssignee: PropTypes.func.isRequired,
|
||||||
|
user: PropTypes.shape({}).isRequired,
|
||||||
|
assigneeUsername: PropTypes.string,
|
||||||
|
};
|
||||||
|
|
||||||
|
Assignee.defaultProps = {
|
||||||
|
assigneeUsername: null,
|
||||||
|
};
|
|
@ -12,10 +12,15 @@ import moment from 'moment';
|
||||||
import template from 'lodash/template';
|
import template from 'lodash/template';
|
||||||
import templateSettings from 'lodash/templateSettings';
|
import templateSettings from 'lodash/templateSettings';
|
||||||
|
|
||||||
import { getTextualSummary, getTitle, getStatus } from '../helpers';
|
import {
|
||||||
import { getData, update } from '../../helpers/http';
|
getTextualSummary,
|
||||||
|
getTitle,
|
||||||
|
getStatus,
|
||||||
|
updateAlertSummary,
|
||||||
|
} from '../helpers';
|
||||||
|
import { getData } from '../../helpers/http';
|
||||||
import { getApiUrl, bzBaseUrl, createQueryParams } from '../../helpers/url';
|
import { getApiUrl, bzBaseUrl, createQueryParams } from '../../helpers/url';
|
||||||
import { endpoints, summaryStatusMap } from '../constants';
|
import { summaryStatusMap } from '../constants';
|
||||||
import DropdownMenuItems from '../../shared/DropdownMenuItems';
|
import DropdownMenuItems from '../../shared/DropdownMenuItems';
|
||||||
|
|
||||||
import AlertModal from './AlertModal';
|
import AlertModal from './AlertModal';
|
||||||
|
@ -111,10 +116,11 @@ export default class StatusDropdown extends React.Component {
|
||||||
changeAlertSummary = async params => {
|
changeAlertSummary = async params => {
|
||||||
const { alertSummary, updateState, updateViewState } = this.props;
|
const { alertSummary, updateState, updateViewState } = this.props;
|
||||||
|
|
||||||
const { data, failureStatus } = await update(
|
const { data, failureStatus } = await updateAlertSummary(
|
||||||
getApiUrl(`${endpoints.alertSummary}${alertSummary.id}/`),
|
alertSummary.id,
|
||||||
params,
|
params,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (failureStatus) {
|
if (failureStatus) {
|
||||||
return updateViewState({
|
return updateViewState({
|
||||||
errorMessages: [
|
errorMessages: [
|
||||||
|
|
|
@ -527,6 +527,9 @@ export const getTitle = alertSummary => {
|
||||||
return title;
|
return title;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const updateAlertSummary = async (alertSummaryId, params) =>
|
||||||
|
update(getApiUrl(`${endpoints.alertSummary}${alertSummaryId}/`), params);
|
||||||
|
|
||||||
export const convertParams = (params, value) =>
|
export const convertParams = (params, value) =>
|
||||||
Boolean(params[value] !== undefined && parseInt(params[value], 10));
|
Boolean(params[value] !== undefined && parseInt(params[value], 10));
|
||||||
|
|
||||||
|
|
Загрузка…
Ссылка в новой задаче