}
interface State {
+ analyzedBefore?: string;
createProjectForm: boolean;
page: number;
projects: Project[];
}
getFilters = () => ({
+ analyzedBefore: this.state.analyzedBefore,
organization: this.props.organization.key,
p: this.state.page !== 1 ? this.state.page : undefined,
ps: PAGE_SIZE,
);
};
+ handleDateChanged = (analyzedBefore?: string) =>
+ this.setState({ ready: false, page: 1, analyzedBefore }, this.requestProjects);
+
onProjectSelected = (project: string) => {
const newSelection = uniq([...this.state.selection, project]);
this.setState({ selection: newSelection });
/>
<Search
+ analyzedBefore={this.state.analyzedBefore}
onAllSelected={this.onAllSelected}
onAllDeselected={this.onAllDeselected}
+ onDateChanged={this.handleDateChanged}
onDeleteProjects={this.requestProjects}
onProvisionedChanged={this.onProvisionedChanged}
onQualifierChanged={this.onQualifierChanged}
import QualifierIcon from '../../components/shared/QualifierIcon';
import { translate } from '../../helpers/l10n';
import { getComponentPermissionsUrl } from '../../helpers/urls';
+import DateTooltipFormatter from '../../components/intl/DateTooltipFormatter';
interface Props {
onApplyTemplateClick: (project: Project) => void;
</Link>
</td>
+ <td className="thin nowrap">
+ {project.visibility === Visibility.Private && <PrivateBadge />}
+ </td>
+
<td className="nowrap">
<span className="note">
{project.key}
</span>
</td>
- <td className="width-20">
- {project.visibility === Visibility.Private && <PrivateBadge />}
+ <td className="thin nowrap text-right">
+ {project.lastAnalysisDate
+ ? <DateTooltipFormatter date={project.lastAnalysisDate} />
+ : <span className="note">—</span>}
</td>
<td className="thin nowrap">
<table
className={classNames('data', 'zebra', { 'new-loading': !this.props.ready })}
id="projects-management-page-projects">
+ <thead>
+ <tr>
+ <th />
+ <th>Name</th>
+ <th />
+ <th>Key</th>
+ <th className="thin nowrap text-right">Last Analysis</th>
+ <th />
+ </tr>
+ </thead>
<tbody>
{this.props.projects.map(project =>
<ProjectRow
import { translate } from '../../helpers/l10n';
import QualifierIcon from '../../components/shared/QualifierIcon';
import Tooltip from '../../components/controls/Tooltip';
+import DateInput from '../../components/controls/DateInput';
export interface Props {
+ analyzedBefore?: string;
onAllDeselected: () => void;
onAllSelected: () => void;
+ onDateChanged: (analyzedBefore?: string) => void;
onDeleteProjects: () => void;
onProvisionedChanged: (provisioned: boolean) => void;
onQualifierChanged: (qualifier: string) => void;
</td>
: null;
+ renderDateFilter = () => {
+ return (
+ <td className="thin nowrap text-middle">
+ <DateInput
+ inputClassName="input-medium"
+ name="analyzed-before"
+ onChange={this.props.onDateChanged}
+ placeholder={translate('analyzed_before')}
+ value={this.props.analyzedBefore}
+ />
+ </td>
+ );
+ };
+
render() {
const isSomethingSelected = this.props.projects.length > 0 && this.props.selection.length > 0;
return (
- <div className="panel panel-vertical bordered-bottom spacer-bottom">
+ <div className="big-spacer-bottom">
<table className="data">
<tbody>
<tr>
{this.props.ready ? this.renderCheckbox() : <i className="spinner" />}
</td>
{this.renderQualifierFilter()}
+ {this.renderDateFilter()}
{this.renderTypeFilter()}
<td className="text-middle">
<form onSubmit={this.onSubmit} className="search-box">
it('renders', () => {
expect(shallowRender()).toMatchSnapshot();
+ expect(
+ shallowRender({ project: { ...project, lastAnalysisDate: '2017-04-08T00:00:00.000Z' } })
+ ).toMatchSnapshot();
});
it('checks project', () => {
expect(wrapper.find('Checkbox[id="projects-provisioned"]').exists()).toBeFalsy();
});
+it('updates analysis date', () => {
+ const onDateChanged = jest.fn();
+ const wrapper = shallowRender({ onDateChanged });
+
+ wrapper.find('DateInput').prop<Function>('onChange')('2017-04-08T00:00:00.000Z');
+ expect(onDateChanged).toBeCalledWith('2017-04-08T00:00:00.000Z');
+
+ wrapper.find('DateInput').prop<Function>('onChange')(undefined);
+ expect(onDateChanged).toBeCalledWith(undefined);
+});
+
it('searches', () => {
const onSearch = jest.fn();
const wrapper = shallowRender({ onSearch });
<Search
onAllDeselected={jest.fn()}
onAllSelected={jest.fn()}
+ onDateChanged={jest.fn()}
onDeleteProjects={jest.fn()}
onProvisionedChanged={jest.fn()}
onQualifierChanged={jest.fn()}
</span>
</Link>
</td>
+ <td
+ className="thin nowrap"
+ >
+ <PrivateBadge />
+ </td>
<td
className="nowrap"
>
</span>
</td>
<td
- className="width-20"
+ className="thin nowrap text-right"
+ >
+ <span
+ className="note"
+ >
+ —
+ </span>
+ </td>
+ <td
+ className="thin nowrap"
+ >
+ <div
+ className="dropdown"
+ >
+ <button
+ className="dropdown-toggle"
+ data-toggle="dropdown"
+ >
+ actions
+
+ <i
+ className="icon-dropdown"
+ />
+ </button>
+ <ul
+ className="dropdown-menu dropdown-menu-right"
+ >
+ <li>
+ <Link
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ to={
+ Object {
+ "pathname": "/project_roles",
+ "query": Object {
+ "id": "project",
+ },
+ }
+ }
+ >
+ edit_permissions
+ </Link>
+ </li>
+ <li>
+ <a
+ className="js-apply-template"
+ href="#"
+ onClick={[Function]}
+ >
+ projects_role.apply_template
+ </a>
+ </li>
+ </ul>
+ </div>
+ </td>
+</tr>
+`;
+
+exports[`renders 2`] = `
+<tr>
+ <td
+ className="thin"
+ >
+ <Checkbox
+ checked={true}
+ onCheck={[Function]}
+ thirdState={false}
+ />
+ </td>
+ <td
+ className="nowrap"
+ >
+ <Link
+ className="link-with-icon"
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ to={
+ Object {
+ "pathname": "/dashboard",
+ "query": Object {
+ "id": "project",
+ },
+ }
+ }
+ >
+ <QualifierIcon
+ qualifier="TRK"
+ />
+
+ <span>
+ Project
+ </span>
+ </Link>
+ </td>
+ <td
+ className="thin nowrap"
>
<PrivateBadge />
</td>
+ <td
+ className="nowrap"
+ >
+ <span
+ className="note"
+ >
+ project
+ </span>
+ </td>
+ <td
+ className="thin nowrap text-right"
+ >
+ <DateTooltipFormatter
+ date="2017-04-08T00:00:00.000Z"
+ />
+ </td>
<td
className="thin nowrap"
>
className="data zebra new-loading"
id="projects-management-page-projects"
>
+ <thead>
+ <tr>
+ <th />
+ <th>
+ Name
+ </th>
+ <th />
+ <th>
+ Key
+ </th>
+ <th
+ className="thin nowrap text-right"
+ >
+ Last Analysis
+ </th>
+ <th />
+ </tr>
+ </thead>
<tbody>
<ProjectRow
onApplyTemplateClick={[Function]}
exports[`render qualifiers filter 1`] = `
<div
- className="panel panel-vertical bordered-bottom spacer-bottom"
+ className="big-spacer-bottom"
>
<table
className="data"
valueRenderer={[Function]}
/>
</td>
+ <td
+ className="thin nowrap text-middle"
+ >
+ <DateInput
+ format="yy-mm-dd"
+ inputClassName="input-medium"
+ name="analyzed-before"
+ onChange={[Function]}
+ placeholder="last_analysis_before"
+ />
+ </td>
<td
className="thin nowrap text-middle"
>
exports[`renders 1`] = `
<div
- className="panel panel-vertical bordered-bottom spacer-bottom"
+ className="big-spacer-bottom"
>
<table
className="data"
thirdState={false}
/>
</td>
+ <td
+ className="thin nowrap text-middle"
+ >
+ <DateInput
+ format="yy-mm-dd"
+ inputClassName="input-medium"
+ name="analyzed-before"
+ onChange={[Function]}
+ placeholder="last_analysis_before"
+ />
+ </td>
<td
className="thin nowrap text-middle"
>
export interface Project {
key: string;
+ lastAnalysisDate?: string;
name: string;
qualifier: string;
visibility: Visibility;
import * as classNames from 'classnames';
import { pick } from 'lodash';
import './styles.css';
+import CloseIcon from '../icons-components/CloseIcon';
interface Props {
className?: string;
- value?: string;
format?: string;
+ inputClassName?: string;
name: string;
+ onChange: (value?: string) => void;
placeholder: string;
- onChange: (value: string) => void;
+ value?: string;
}
export default class DateInput extends React.PureComponent<Props> {
input: HTMLInputElement;
static defaultProps = {
- value: '',
format: 'yy-mm-dd'
};
this.attachDatePicker();
}
- componentWillReceiveProps(nextProps: Props) {
- if (nextProps.value != null && this.input) {
- this.input.value = nextProps.value;
- }
- }
-
- handleChange() {
+ handleChange = () => {
const { value } = this.input;
this.props.onChange(value);
- }
+ };
+
+ handleResetClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => {
+ event.preventDefault();
+ event.currentTarget.blur();
+ this.props.onChange(undefined);
+ };
attachDatePicker() {
const opts = {
dateFormat: this.props.format,
changeMonth: true,
changeYear: true,
- onSelect: this.handleChange.bind(this)
+ onSelect: this.handleChange
};
if ($.fn && ($.fn as any).datepicker && this.input) {
return (
<span className={classNames('date-input-control', this.props.className)}>
<input
- className="date-input-control-input"
- ref={node => (this.input = node as HTMLInputElement)}
- type="text"
- defaultValue={this.props.value}
+ className={classNames('date-input-control-input', this.props.inputClassName)}
+ onChange={this.handleChange}
readOnly={true}
+ ref={node => (this.input = node!)}
+ type="text"
+ value={this.props.value || ''}
{...inputProps}
/>
<span className="date-input-control-icon">
<path d="M5.5 6h2v2h-2V6zm3 0h2v2h-2V6zm3 0h2v2h-2V6zm-9 6h2v2h-2v-2zm3 0h2v2h-2v-2zm3 0h2v2h-2v-2zm-3-3h2v2h-2V9zm3 0h2v2h-2V9zm3 0h2v2h-2V9zm-9 0h2v2h-2V9zm11-9v1h-2V0h-7v1h-2V0h-2v16h15V0h-2zm1 15h-13V4h13v11z" />
</svg>
</span>
+ {this.props.value != undefined &&
+ <a className="date-input-control-reset" href="#" onClick={this.handleResetClick}>
+ <CloseIcon className="" />
+ </a>}
</span>
);
}
}
.date-input-control-input {
- width: 105px;
+ width: 130px;
padding-left: 24px !important;
cursor: pointer;
}
fill: #4b9fd5;
}
+.date-input-control-reset {
+ position: absolute;
+ top: 4px;
+ right: 4px;
+ border: none;
+}
+
.boolean-toggle {
display: inline-block;
vertical-align: middle;
added_since_version=Added since version {0}
all_violations=All violations
all_issues=All issues
+analyzed_before=Analyzed before
and_worse=and worse
are_you_sure=Are you sure?
assigned_to=Assigned to
}
public ProjectsManagementPage shouldHaveProjectsCount(int count) {
- $$("#projects-management-page-projects tr").shouldHaveSize(count);
+ $$("#projects-management-page-projects tbody tr").shouldHaveSize(count);
return this;
}