this.props.router.push({ pathname: this.props.location.pathname });
};
+ handleFavorite = (key: string, isFavorite: boolean) => {
+ this.setState(({ projects }) => {
+ if (!projects) {
+ return null;
+ }
+
+ return {
+ projects: projects.map(p => (p.key === key ? { ...p, isFavorite } : p))
+ };
+ });
+ };
+
handlePerspectiveChange = ({ view, visualization }: { view: string; visualization?: string }) => {
const { storageOptionsSuffix } = this.props;
const query: {
<ProjectsList
cardType={this.getView()}
currentUser={this.props.currentUser}
+ handleFavorite={this.handleFavorite}
isFavorite={this.props.isFavorite}
isFiltered={hasFilterParams(this.state.query)}
organization={this.props.organization}
import { Project } from '../types';
interface Props {
+ handleFavorite: (component: string, isFavorite: boolean) => void;
height: number;
organization: T.Organization | undefined;
project: Project;
type?: string;
}
-export default function ProjectCard(props: Props) {
- if (props.type === 'leak') {
- return <ProjectCardLeak {...props} />;
+export default class ProjectCard extends React.PureComponent<Props> {
+ render() {
+ if (this.props.type === 'leak') {
+ return <ProjectCardLeak {...this.props} />;
+ }
+ return <ProjectCardOverall {...this.props} />;
}
- return <ProjectCardOverall {...props} />;
}
import { getProjectUrl } from '../../../helpers/urls';
interface Props {
+ handleFavorite: (component: string, isFavorite: boolean) => void;
height: number;
organization: T.Organization | undefined;
project: Project;
}
-export default function ProjectCardLeak({ height, organization, project }: Props) {
- const { measures } = project;
- const hasTags = project.tags.length > 0;
- const periodMs = project.leakPeriodDate ? difference(Date.now(), project.leakPeriodDate) : 0;
+export default class ProjectCardLeak extends React.PureComponent<Props> {
+ render() {
+ const { handleFavorite, height, organization, project } = this.props;
+ const { measures } = project;
+ const hasTags = project.tags.length > 0;
+ const periodMs = project.leakPeriodDate ? difference(Date.now(), project.leakPeriodDate) : 0;
- return (
- <div className="boxed-group project-card" data-key={project.key} style={{ height }}>
- <div className="boxed-group-header clearfix">
- <div className="project-card-header">
- {project.isFavorite != null && (
- <Favorite
- className="spacer-right"
- component={project.key}
- favorite={project.isFavorite}
- qualifier="TRK"
- />
- )}
- <h2 className="project-card-name">
- {!organization && (
- <ProjectCardOrganizationContainer organization={project.organization} />
+ return (
+ <div className="boxed-group project-card" data-key={project.key} style={{ height }}>
+ <div className="boxed-group-header clearfix">
+ <div className="project-card-header">
+ {project.isFavorite != null && (
+ <Favorite
+ className="spacer-right"
+ component={project.key}
+ favorite={project.isFavorite}
+ handleFavorite={handleFavorite}
+ qualifier="TRK"
+ />
)}
- <Link to={{ pathname: '/dashboard', query: { id: project.key } }}>{project.name}</Link>
- </h2>
- {project.analysisDate && <ProjectCardQualityGate status={measures['alert_status']} />}
- <div className="project-card-header-right">
- <PrivacyBadgeContainer
- className="spacer-left"
- organization={organization || project.organization}
- qualifier="TRK"
- tooltipProps={{ projectKey: project.key }}
- visibility={project.visibility}
- />
+ <h2 className="project-card-name">
+ {!organization && (
+ <ProjectCardOrganizationContainer organization={project.organization} />
+ )}
+ <Link to={{ pathname: '/dashboard', query: { id: project.key } }}>
+ {project.name}
+ </Link>
+ </h2>
+ {project.analysisDate && <ProjectCardQualityGate status={measures['alert_status']} />}
+ <div className="project-card-header-right">
+ <PrivacyBadgeContainer
+ className="spacer-left"
+ organization={organization || project.organization}
+ qualifier="TRK"
+ tooltipProps={{ projectKey: project.key }}
+ visibility={project.visibility}
+ />
- {hasTags && <TagsList className="spacer-left note" tags={project.tags} />}
+ {hasTags && <TagsList className="spacer-left note" tags={project.tags} />}
+ </div>
</div>
+ {project.analysisDate && project.leakPeriodDate && (
+ <div className="project-card-dates note text-right pull-right">
+ <span className="project-card-leak-date pull-right">
+ {translateWithParameters('projects.new_code_period_x', formatDuration(periodMs))}
+ </span>
+ <DateTimeFormatter date={project.analysisDate}>
+ {formattedDate => (
+ <span>
+ {translateWithParameters('projects.last_analysis_on_x', formattedDate)}
+ </span>
+ )}
+ </DateTimeFormatter>
+ </div>
+ )}
</div>
- {project.analysisDate && project.leakPeriodDate && (
- <div className="project-card-dates note text-right pull-right">
- <span className="project-card-leak-date pull-right">
- {translateWithParameters('projects.new_code_period_x', formatDuration(periodMs))}
- </span>
- <DateTimeFormatter date={project.analysisDate}>
- {formattedDate => (
- <span>{translateWithParameters('projects.last_analysis_on_x', formattedDate)}</span>
+
+ {project.analysisDate && project.leakPeriodDate ? (
+ <div className="boxed-group-inner">
+ <ProjectCardLeakMeasures measures={measures} />
+ </div>
+ ) : (
+ <div className="boxed-group-inner">
+ <div className="project-card-not-analyzed">
+ <span className="note">
+ {project.analysisDate
+ ? translate('projects.no_new_code_period')
+ : translate('projects.not_analyzed')}
+ </span>
+ {!project.analysisDate && (
+ <Link className="button spacer-left" to={getProjectUrl(project.key)}>
+ {translate('projects.configure_analysis')}
+ </Link>
)}
- </DateTimeFormatter>
+ </div>
</div>
)}
</div>
-
- {project.analysisDate && project.leakPeriodDate ? (
- <div className="boxed-group-inner">
- <ProjectCardLeakMeasures measures={measures} />
- </div>
- ) : (
- <div className="boxed-group-inner">
- <div className="project-card-not-analyzed">
- <span className="note">
- {project.analysisDate
- ? translate('projects.no_new_code_period')
- : translate('projects.not_analyzed')}
- </span>
- {!project.analysisDate && (
- <Link className="button spacer-left" to={getProjectUrl(project.key)}>
- {translate('projects.configure_analysis')}
- </Link>
- )}
- </div>
- </div>
- )}
- </div>
- );
+ );
+ }
}
import { getProjectUrl } from '../../../helpers/urls';
interface Props {
+ handleFavorite: (component: string, isFavorite: boolean) => void;
height: number;
organization: T.Organization | undefined;
project: Project;
}
-export default function ProjectCardOverall({ height, organization, project }: Props) {
- const { measures } = project;
+export default class ProjectCardOverall extends React.PureComponent<Props> {
+ render() {
+ const { handleFavorite, height, organization, project } = this.props;
+ const { measures } = project;
- const hasTags = project.tags.length > 0;
+ const hasTags = project.tags.length > 0;
- return (
- <div className="boxed-group project-card" data-key={project.key} style={{ height }}>
- <div className="boxed-group-header clearfix">
- <div className="project-card-header">
- {project.isFavorite !== undefined && (
- <Favorite
- className="spacer-right"
- component={project.key}
- favorite={project.isFavorite}
- qualifier="TRK"
- />
- )}
- <h2 className="project-card-name">
- {!organization && (
- <ProjectCardOrganizationContainer organization={project.organization} />
+ return (
+ <div className="boxed-group project-card" data-key={project.key} style={{ height }}>
+ <div className="boxed-group-header clearfix">
+ <div className="project-card-header">
+ {project.isFavorite !== undefined && (
+ <Favorite
+ className="spacer-right"
+ component={project.key}
+ favorite={project.isFavorite}
+ handleFavorite={handleFavorite}
+ qualifier="TRK"
+ />
)}
- <Link to={getProjectUrl(project.key)}>{project.name}</Link>
- </h2>
- {project.analysisDate && <ProjectCardQualityGate status={measures['alert_status']} />}
- <div className="project-card-header-right">
- <PrivacyBadgeContainer
- className="spacer-left"
- organization={organization || project.organization}
- qualifier="TRK"
- tooltipProps={{ projectKey: project.key }}
- visibility={project.visibility}
- />
- {hasTags && <TagsList className="spacer-left note" tags={project.tags} />}
+ <h2 className="project-card-name">
+ {!organization && (
+ <ProjectCardOrganizationContainer organization={project.organization} />
+ )}
+ <Link to={getProjectUrl(project.key)}>{project.name}</Link>
+ </h2>
+ {project.analysisDate && <ProjectCardQualityGate status={measures['alert_status']} />}
+ <div className="project-card-header-right">
+ <PrivacyBadgeContainer
+ className="spacer-left"
+ organization={organization || project.organization}
+ qualifier="TRK"
+ tooltipProps={{ projectKey: project.key }}
+ visibility={project.visibility}
+ />
+ {hasTags && <TagsList className="spacer-left note" tags={project.tags} />}
+ </div>
</div>
+ {project.analysisDate && (
+ <div className="project-card-dates note text-right">
+ <DateTimeFormatter date={project.analysisDate}>
+ {formattedDate => (
+ <span className="big-spacer-left">
+ {translateWithParameters('projects.last_analysis_on_x', formattedDate)}
+ </span>
+ )}
+ </DateTimeFormatter>
+ </div>
+ )}
</div>
- {project.analysisDate && (
- <div className="project-card-dates note text-right">
- <DateTimeFormatter date={project.analysisDate}>
- {formattedDate => (
- <span className="big-spacer-left">
- {translateWithParameters('projects.last_analysis_on_x', formattedDate)}
- </span>
- )}
- </DateTimeFormatter>
+
+ {project.analysisDate ? (
+ <div className="boxed-group-inner">
+ {<ProjectCardOverallMeasures measures={measures} />}
+ </div>
+ ) : (
+ <div className="boxed-group-inner">
+ <div className="project-card-not-analyzed">
+ <span className="note">{translate('projects.not_analyzed')}</span>
+ <Link className="button spacer-left" to={getProjectUrl(project.key)}>
+ {translate('projects.configure_analysis')}
+ </Link>
+ </div>
</div>
)}
</div>
-
- {project.analysisDate ? (
- <div className="boxed-group-inner">
- {<ProjectCardOverallMeasures measures={measures} />}
- </div>
- ) : (
- <div className="boxed-group-inner">
- <div className="project-card-not-analyzed">
- <span className="note">{translate('projects.not_analyzed')}</span>
- <Link className="button spacer-left" to={getProjectUrl(project.key)}>
- {translate('projects.configure_analysis')}
- </Link>
- </div>
- </div>
- )}
- </div>
- );
+ );
+ }
}
interface Props {
cardType?: string;
currentUser: T.CurrentUser;
+ handleFavorite: (component: string, isFavorite: boolean) => void;
isFavorite: boolean;
isFiltered: boolean;
organization: T.Organization | undefined;
return (
<div key={key} style={{ ...style, height }}>
<ProjectCard
+ handleFavorite={this.props.handleFavorite}
height={height}
key={project.key}
organization={this.props.organization}
expect(wrapper).toMatchSnapshot();
});
+it('handles favorite projects', () => {
+ const wrapper = shallowRender();
+ expect(wrapper.state('projects')).toMatchSnapshot();
+
+ wrapper.instance().handleFavorite('foo', true);
+ expect(wrapper.state('projects')).toMatchSnapshot();
+});
+
function shallowRender(
props: Partial<AllProjects['props']> = {},
push = jest.fn(),
replace = jest.fn()
) {
- const wrapper = shallow(
+ const wrapper = shallow<AllProjects>(
<AllProjects
currentUser={{ isLoggedIn: true }}
isFavorite={false}
);
wrapper.setState({
loading: false,
- projects: [{ key: 'foo', measures: {}, name: 'Foo' }],
+ projects: [{ key: 'foo', measures: {}, name: 'Foo', tags: [], visibility: 'public' }],
total: 0
});
return wrapper;
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import * as React from 'react';
+import { shallow } from 'enzyme';
+import { Project } from '../../types';
+import ProjectCard from '../ProjectCard';
+
+const ORGANIZATION = { key: 'org', name: 'org' };
+
+const MEASURES = {
+ alert_status: 'OK',
+ reliability_rating: '1.0',
+ sqale_rating: '1.0',
+ new_bugs: '12'
+};
+
+const PROJECT: Project = {
+ analysisDate: '2017-01-01',
+ leakPeriodDate: '2016-12-01',
+ key: 'foo',
+ measures: MEASURES,
+ name: 'Foo',
+ organization: { key: 'org', name: 'org' },
+ tags: [],
+ visibility: 'public'
+};
+
+it('should show <ProjectCardOverall/> by default', () => {
+ const wrapper = shallowRender();
+ expect(wrapper.find('ProjectCardOverall')).toBeTruthy();
+ expect(wrapper.find('ProjectCardLeak')).toBeTruthy();
+});
+
+it('should show <ProjectCardLeak/> when asked', () => {
+ const wrapper = shallowRender();
+ expect(wrapper.find('ProjectCardLeak')).toBeTruthy();
+ expect(wrapper.find('ProjectCardOverall')).toBeTruthy();
+});
+
+function shallowRender(type?: string) {
+ return shallow(
+ <ProjectCard
+ handleFavorite={jest.fn}
+ height={200}
+ organization={ORGANIZATION}
+ project={PROJECT}
+ type={type}
+ />
+ );
+}
};
it('should display analysis date and leak start date', () => {
- const card = shallow(<ProjectCardLeak height={100} organization={undefined} project={PROJECT} />);
+ const card = shallowRender(PROJECT);
expect(card.find('.project-card-dates').exists()).toBeTruthy();
expect(card.find('.project-card-dates').find('.project-card-leak-date')).toHaveLength(1);
expect(card.find('.project-card-dates').find('DateTimeFormatter')).toHaveLength(1);
it('should not display analysis date or leak start date', () => {
const project = { ...PROJECT, analysisDate: undefined };
- const card = shallow(<ProjectCardLeak height={100} organization={undefined} project={project} />);
+ const card = shallowRender(project);
expect(card.find('.project-card-dates').exists()).toBeFalsy();
});
it('should display tags', () => {
const project = { ...PROJECT, tags: ['foo', 'bar'] };
expect(
- shallow(<ProjectCardLeak height={100} organization={undefined} project={project} />)
+ shallowRender(project)
.find('TagsList')
.exists()
).toBeTruthy();
it('should display private badge', () => {
const project: Project = { ...PROJECT, visibility: 'private' };
expect(
- shallow(<ProjectCardLeak height={100} organization={undefined} project={project} />)
+ shallowRender(project)
.find('Connect(PrivacyBadge)')
.exists()
).toBeTruthy();
});
it('should display the leak measures and quality gate', () => {
- expect(
- shallow(<ProjectCardLeak height={100} organization={undefined} project={PROJECT} />)
- ).toMatchSnapshot();
+ expect(shallowRender(PROJECT)).toMatchSnapshot();
});
it('should display not analyzed yet', () => {
- expect(
- shallow(
- <ProjectCardLeak
- height={100}
- organization={undefined}
- project={{ ...PROJECT, analysisDate: undefined }}
- />
- )
- ).toMatchSnapshot();
+ expect(shallowRender({ ...PROJECT, analysisDate: undefined })).toMatchSnapshot();
});
+
+function shallowRender(project: Project) {
+ return shallow(
+ <ProjectCardLeak
+ handleFavorite={jest.fn()}
+ height={100}
+ organization={undefined}
+ project={project}
+ />
+ );
+}
it('should display analysis date (and not leak period) when defined', () => {
expect(
- shallow(<ProjectCardOverall height={100} organization={undefined} project={PROJECT} />)
+ shallowRender(PROJECT)
.find('.project-card-dates')
.exists()
).toBeTruthy();
expect(
- shallow(
- <ProjectCardOverall
- height={100}
- organization={undefined}
- project={{ ...PROJECT, analysisDate: undefined }}
- />
- )
+ shallowRender({ ...PROJECT, analysisDate: undefined })
.find('.project-card-dates')
.exists()
).toBeFalsy();
it('should not display the quality gate', () => {
const project = { ...PROJECT, analysisDate: undefined };
expect(
- shallow(<ProjectCardOverall height={100} organization={undefined} project={project} />)
+ shallowRender(project)
.find('ProjectCardOverallQualityGate')
.exists()
).toBeFalsy();
it('should display tags', () => {
const project = { ...PROJECT, tags: ['foo', 'bar'] };
expect(
- shallow(<ProjectCardOverall height={100} organization={undefined} project={project} />)
+ shallowRender(project)
.find('TagsList')
.exists()
).toBeTruthy();
it('should display private badge', () => {
const project: Project = { ...PROJECT, visibility: 'private' };
expect(
- shallow(<ProjectCardOverall height={100} organization={undefined} project={project} />)
+ shallowRender(project)
.find('Connect(PrivacyBadge)')
.exists()
).toBeTruthy();
});
it('should display the overall measures and quality gate', () => {
- expect(
- shallow(<ProjectCardOverall height={100} organization={undefined} project={PROJECT} />)
- ).toMatchSnapshot();
+ expect(shallowRender(PROJECT)).toMatchSnapshot();
});
it('should display not analyzed yet', () => {
- expect(
- shallow(
- <ProjectCardOverall
- height={100}
- organization={undefined}
- project={{ ...PROJECT, analysisDate: undefined }}
- />
- )
- ).toMatchSnapshot();
+ expect(shallowRender({ ...PROJECT, analysisDate: undefined })).toMatchSnapshot();
});
+
+function shallowRender(project: Project) {
+ return shallow(
+ <ProjectCardOverall
+ handleFavorite={jest.fn()}
+ height={100}
+ organization={undefined}
+ project={project}
+ />
+ );
+}
// Jest Snapshot v1, https://goo.gl/fbAQLP
+exports[`handles favorite projects 1`] = `
+Array [
+ Object {
+ "key": "foo",
+ "measures": Object {},
+ "name": "Foo",
+ "tags": Array [],
+ "visibility": "public",
+ },
+]
+`;
+
+exports[`handles favorite projects 2`] = `
+Array [
+ Object {
+ "isFavorite": true,
+ "key": "foo",
+ "measures": Object {},
+ "name": "Foo",
+ "tags": Array [],
+ "visibility": "public",
+ },
+]
+`;
+
exports[`renders 1`] = `
<div
className="layout-page projects-page"
"key": "foo",
"measures": Object {},
"name": "Foo",
+ "tags": Array [],
+ "visibility": "public",
},
]
}
"isLoggedIn": true,
}
}
+ handleFavorite={[Function]}
isFavorite={false}
isFiltered={false}
projects={
"key": "foo",
"measures": Object {},
"name": "Foo",
+ "tags": Array [],
+ "visibility": "public",
},
]
}
"key": "foo",
"measures": Object {},
"name": "Foo",
+ "tags": Array [],
+ "visibility": "public",
},
]
}
"key": "foo",
"measures": Object {},
"name": "Foo",
+ "tags": Array [],
+ "visibility": "public",
},
]
}
"isLoggedIn": true,
}
}
+ handleFavorite={[Function]}
isFavorite={false}
isFiltered={false}
organization={
component: string;
favorite: boolean;
qualifier: string;
+ handleFavorite?: (component: string, isFavorite: boolean) => void;
}
-export default function Favorite({ component, ...other }: Props) {
- return (
- <FavoriteBase
- {...other}
- addFavorite={() => addFavorite(component)}
- removeFavorite={() => removeFavorite(component)}
- />
- );
+export default class Favorite extends React.PureComponent<Props> {
+ callback = (isFavorite: boolean) =>
+ this.props.handleFavorite && this.props.handleFavorite(this.props.component, isFavorite);
+
+ add = () => {
+ return addFavorite(this.props.component).then(() => this.callback(true));
+ };
+
+ remove = () => {
+ return removeFavorite(this.props.component).then(() => this.callback(false));
+ };
+
+ render() {
+ const { component, handleFavorite, ...other } = this.props;
+ return <FavoriteBase {...other} addFavorite={this.add} removeFavorite={this.remove} />;
+ }
}
this.mounted = true;
}
- componentWillReceiveProps(nextProps: Props) {
- if (nextProps.favorite !== this.props.favorite || nextProps.favorite !== this.state.favorite) {
- this.setState({ favorite: nextProps.favorite });
- }
- }
-
componentWillUnmount() {
this.mounted = false;
}
import { shallow } from 'enzyme';
import Favorite from '../Favorite';
+jest.mock('../../../api/favorites', () => ({
+ addFavorite: jest.fn(() => Promise.resolve()),
+ removeFavorite: jest.fn(() => Promise.resolve())
+}));
+
it('renders', () => {
- expect(shallow(<Favorite component="foo" favorite={true} qualifier="TRK" />)).toMatchSnapshot();
+ expect(shallowRender()).toMatchSnapshot();
+});
+
+it('calls handleFavorite when given', async () => {
+ const handleFavorite = jest.fn();
+ const wrapper = shallowRender(handleFavorite);
+ const favoriteBase = wrapper.find('FavoriteBase');
+ const addFavorite = favoriteBase.prop<Function>('addFavorite');
+ const removeFavorite = favoriteBase.prop<Function>('removeFavorite');
+
+ removeFavorite();
+ await new Promise(setImmediate);
+ expect(handleFavorite).toHaveBeenCalledWith('foo', false);
+
+ addFavorite();
+ await new Promise(setImmediate);
+ expect(handleFavorite).toHaveBeenCalledWith('foo', true);
});
+
+function shallowRender(handleFavorite?: () => void) {
+ return shallow(
+ <Favorite component="foo" favorite={true} handleFavorite={handleFavorite} qualifier="TRK" />
+ );
+}