]> source.dussan.org Git - sonarqube.git/commitdiff
SONARCLOUD-669 Fix favorite star behavior
authorSiegfried Ehret <49895321+siegfried-ehret-sonarsource@users.noreply.github.com>
Mon, 27 May 2019 08:40:59 +0000 (10:40 +0200)
committerSonarTech <sonartech@sonarsource.com>
Mon, 27 May 2019 18:21:10 +0000 (20:21 +0200)
13 files changed:
server/sonar-web/src/main/js/apps/projects/components/AllProjects.tsx
server/sonar-web/src/main/js/apps/projects/components/ProjectCard.tsx
server/sonar-web/src/main/js/apps/projects/components/ProjectCardLeak.tsx
server/sonar-web/src/main/js/apps/projects/components/ProjectCardOverall.tsx
server/sonar-web/src/main/js/apps/projects/components/ProjectsList.tsx
server/sonar-web/src/main/js/apps/projects/components/__tests__/AllProjects-test.tsx
server/sonar-web/src/main/js/apps/projects/components/__tests__/ProjectCard-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projects/components/__tests__/ProjectCardLeak-test.tsx
server/sonar-web/src/main/js/apps/projects/components/__tests__/ProjectCardOverall-test.tsx
server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/AllProjects-test.tsx.snap
server/sonar-web/src/main/js/components/controls/Favorite.tsx
server/sonar-web/src/main/js/components/controls/FavoriteBase.tsx
server/sonar-web/src/main/js/components/controls/__tests__/Favorite-test.tsx

index 8c52c9be55c961cb7194a3bcd51901542baa61c2..70b8d4657a13f417fd3c5f6108d0aa503bf18914 100644 (file)
@@ -172,6 +172,18 @@ export class AllProjects extends React.PureComponent<Props, State> {
     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: {
@@ -317,6 +329,7 @@ export class AllProjects extends React.PureComponent<Props, State> {
               <ProjectsList
                 cardType={this.getView()}
                 currentUser={this.props.currentUser}
+                handleFavorite={this.handleFavorite}
                 isFavorite={this.props.isFavorite}
                 isFiltered={hasFilterParams(this.state.query)}
                 organization={this.props.organization}
index 034f91cf3cceed7699c80b5fea1364ad4ec95ebf..c72bc780b65a9d54936f22172f9ce5520ebc430a 100644 (file)
@@ -23,15 +23,18 @@ import ProjectCardOverall from './ProjectCardOverall';
 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} />;
 }
index cf0bce1ac04464f87fddf9f73306561d2569b37d..beb338949ffce2e7345e0401a58b93bc74eb6b71 100644 (file)
@@ -33,81 +33,90 @@ import { formatDuration } from '../utils';
 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>
-  );
+    );
+  }
 }
index c494faa25b4a8cc99d1766c4a43f4b4a1241428d..214c6970ffb923a5b8afa0e687ec9619ab9995aa 100644 (file)
@@ -31,73 +31,78 @@ import { Project } from '../types';
 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>
-  );
+    );
+  }
 }
index ac05a3a58623b2fbd4eb2a6468e2ad79601b188d..7225fdedf8500f3a35cebac5ef32224504ea7282 100644 (file)
@@ -33,6 +33,7 @@ import { OnboardingContext } from '../../../app/components/OnboardingContext';
 interface Props {
   cardType?: string;
   currentUser: T.CurrentUser;
+  handleFavorite: (component: string, isFavorite: boolean) => void;
   isFavorite: boolean;
   isFiltered: boolean;
   organization: T.Organization | undefined;
@@ -75,6 +76,7 @@ export default class ProjectsList extends React.PureComponent<Props> {
     return (
       <div key={key} style={{ ...style, height }}>
         <ProjectCard
+          handleFavorite={this.props.handleFavorite}
           height={height}
           key={project.key}
           organization={this.props.organization}
index fac7ae2096c2101a85d16d5fe91087be51687683..51910cc0d8446afae0145907245a1d1a89232712 100644 (file)
@@ -188,12 +188,20 @@ it('renders correctly empty organization', async () => {
   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}
@@ -205,7 +213,7 @@ function shallowRender(
   );
   wrapper.setState({
     loading: false,
-    projects: [{ key: 'foo', measures: {}, name: 'Foo' }],
+    projects: [{ key: 'foo', measures: {}, name: 'Foo', tags: [], visibility: 'public' }],
     total: 0
   });
   return wrapper;
diff --git a/server/sonar-web/src/main/js/apps/projects/components/__tests__/ProjectCard-test.tsx b/server/sonar-web/src/main/js/apps/projects/components/__tests__/ProjectCard-test.tsx
new file mode 100644 (file)
index 0000000..e5ef944
--- /dev/null
@@ -0,0 +1,67 @@
+/*
+ * 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}
+    />
+  );
+}
index 7e455598b3c42c0d48718aa83fb32b56a36d203d..59df2a1b6e5ec7868d921227f9856228353c1b21 100644 (file)
@@ -46,7 +46,7 @@ const PROJECT: Project = {
 };
 
 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);
@@ -54,14 +54,14 @@ it('should display analysis date and leak start date', () => {
 
 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();
@@ -70,26 +70,27 @@ it('should display tags', () => {
 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}
+    />
+  );
+}
index bc3eef5411de4ebb2447582d3f3ff8ed19903168..827d89e4aef8a0546391e25f748f0e2b6d8c8d5d 100644 (file)
@@ -41,18 +41,12 @@ const 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();
@@ -61,7 +55,7 @@ it('should display analysis date (and not leak period) when defined', () => {
 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();
@@ -70,7 +64,7 @@ it('should not display the quality gate', () => {
 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();
@@ -79,26 +73,27 @@ it('should display tags', () => {
 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}
+    />
+  );
+}
index d41f0b2801de0aa16399d388b3aa1021318e21ba..28726c14b968c25d3cc7f0f935bde9b845414a46 100644 (file)
@@ -1,5 +1,30 @@
 // 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"
@@ -50,6 +75,8 @@ exports[`renders 1`] = `
                   "key": "foo",
                   "measures": Object {},
                   "name": "Foo",
+                  "tags": Array [],
+                  "visibility": "public",
                 },
               ]
             }
@@ -98,6 +125,7 @@ exports[`renders 1`] = `
               "isLoggedIn": true,
             }
           }
+          handleFavorite={[Function]}
           isFavorite={false}
           isFiltered={false}
           projects={
@@ -106,6 +134,8 @@ exports[`renders 1`] = `
                 "key": "foo",
                 "measures": Object {},
                 "name": "Foo",
+                "tags": Array [],
+                "visibility": "public",
               },
             ]
           }
@@ -195,6 +225,8 @@ exports[`renders 2`] = `
                   "key": "foo",
                   "measures": Object {},
                   "name": "Foo",
+                  "tags": Array [],
+                  "visibility": "public",
                 },
               ]
             }
@@ -226,6 +258,8 @@ exports[`renders 2`] = `
                 "key": "foo",
                 "measures": Object {},
                 "name": "Foo",
+                "tags": Array [],
+                "visibility": "public",
               },
             ]
           }
@@ -391,6 +425,7 @@ exports[`renders correctly empty organization 3`] = `
               "isLoggedIn": true,
             }
           }
+          handleFavorite={[Function]}
           isFavorite={false}
           isFiltered={false}
           organization={
index db7765c2f65e940323d86e2d43446a2e4a8255f3..59e3028ac154432b1b32945189c92e62aa20eba7 100644 (file)
@@ -26,14 +26,23 @@ interface Props {
   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} />;
+  }
 }
index 0566fbdfd8501d38d0912fffae479377ba8dd50f..10865ee16c708282a3bf8a8ab30cd870812a9a11 100644 (file)
@@ -47,12 +47,6 @@ export default class FavoriteBase extends React.PureComponent<Props, State> {
     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;
   }
index c57716e3871d12de937b861eaeb69419c96c9cf2..009f15f6e3049ce4535cf0b34ad561a8b847bda7 100644 (file)
@@ -21,6 +21,33 @@ import * as React from 'react';
 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" />
+  );
+}